• 【Java】构造方法及类的初始化


    一. 利用构造方法给对象初始化

    1. 构造方法的概念

    构造方法(也称为构造器)是一个特殊的成员方法,其名字必须与类名相同,在创建对象时,由编译器自动调用,并且在整个对象的生命周期内只调用一次。

    构造方法的作用就是给对象中的成员进行初始化,并不负责给对象开辟空间。

    1. public class Date {
    2. public int year;
    3. public int month;
    4. public int day;
    5. // 构造方法:
    6. // 名字与类名相同,没有返回值类型,设置为void也不行
    7. // 一般情况下使用public修饰
    8. // 在创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次
    9. public Date(int year, int month, int day) {
    10. this.year = year;
    11. this.month = month;
    12. this.day = day;
    13. System.out.println("Date(int,int,int)方法被调用了");
    14. }
    15. public void printDate() {
    16. System.out.println(year + "-" + month + "-" + day);
    17. }
    18. public static void main(String[] args) {
    19. // 此处创建了一个Date类型的对象,并没有显式调用构造方法
    20. Date d = new Date(2021, 6, 9);
    21. // 输出Date(int,int,int)方法被调用了
    22. d.printDate(); // 2021-6-9
    23. }
    24. }

    2. 构造方法的特性

    1.名字必须与类名相同
    3.没有返回值类型,设置为void也不行
    3.创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次
    4.绝大多数情况下使用public来修饰,特殊场景下会被private修饰
    5.构造方法可以重载(用户根据自己的需求提供不同参数的构造方法); 下面两个构造方法:名字相同,参数列表不同,因此构成了方法重载

    1. public class Date {
    2. public int year;
    3. public int month;
    4. public int day;
    5. // 无参构造方法
    6. public Date(){
    7. this.year = 1900;
    8. this.month = 1;
    9. this.day = 1;
    10. }
    11. // 带有三个参数的构造方法
    12. public Date(int year, int month, int day) {
    13. this.year = year;
    14. this.month = month;
    15. this.day = day;
    16. }
    17. public void printDate(){
    18. System.out.println(year + "-" + month + "-" + day);
    19. }
    20. public static void main(String[] args) {
    21. Date d = new Date();
    22. d.printDate();
    23. }
    24. }

    6.如果用户没有显式定义,编译器会生成一份默认的构造方法,生成的默认构造方法一定是无参的; 一旦用户定义,编译器则不再生成;下面代码中,没有定义任何构造方法,编译器会默认生成一个不带参数的构造方法。

    1. public class Date {
    2. public int year;
    3. public int month;
    4. public int day;
    5. public void printDate(){
    6. System.out.println(year + "-" + month + "-" + day);
    7. }
    8. public static void main(String[] args) {
    9. Date d = new Date();
    10. d.printDate();
    11. }
    12. }

    7.构造方法中,可以通过this调用其他构造方法来简化代码
    【注意事项】

    • 构造方法中,通过this(…)去调用其他构造方法,这条语句必须是构造方法中第一条语句
    • 多个构造方法不可以互相调用(不能形成环), 会形成构造器的递归调用,但却没有调用的结束条件

    1. public class Date {
    2. public int year;
    3. public int month;
    4. public int day;
    5. // 无参构造方法--内部给各个成员赋值初始值,该部分功能与三个参数的构造方法重复
    6. // 此处可以在无参构造方法中通过this调用带有三个参数的构造方法
    7. // 但是this(2022,8,16);必须是构造方法中第一条语句
    8. public Date(){
    9. //System.out.println(year); 注释取消掉,编译会失败
    10. this(2022, 8, 16);
    11. //this.year = 1900;
    12. //this.month = 1;
    13. //this.day = 1;
    14. }
    15. // 带有三个参数的构造方法
    16. public Date(int year, int month, int day) {
    17. this.year = year;
    18. this.month = month;
    19. this.day = day;
    20. }
    21. }

    3. 子类构造方法

    在继承基础上,子类对象构造时,需要先调用基类构造方法,然后执行子类的构造方法。

    在子类构造方法中,并没有写任何关于基类构造的代码,但是在构造子类对象时,先执行基类的构造方法,然后执行子类的构造方法,

    原因在于:子类对象中成员是有两部分组成的,基类继承下来的以及子类新增加的部分 。父类和子类, 肯定是先有父再有子,所以在构造子类对象时候 ,子类构造方法中先要调用基类的构造方法,将从基类继承下来的成员构造完整 ,然后再完成子类自己的构造,将子类自己新增加的成员初始化完整 。

    【注意事项】

    1.若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类构造方法

    1. public class Base {
    2. public Base(){
    3. System.out.println("Base()");
    4. }
    5. }
    6. public class Derived extends Base{
    7. public Derived(){
    8. // super(); // 注意子类构造方法中默认会调用基类的无参构造方法:super(),
    9. // 用户没有写时,编译器会自动添加,而且super()必须是子类构造方法中第一条语句,
    10. // 并且只能出现一次
    11. System.out.println("Derived()");
    12. }
    13. }
    14. public class Test {
    15. public static void main(String[] args) {
    16. Derived d = new Derived();
    17. }
    18. }

    2.如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败。

    1. public class Animal {
    2. public String name;
    3. public int age;
    4. public Animal(String name, int age) {
    5. this.name = name;
    6. this.age = age;
    7. System.out.println("Animal(String , int )");
    8. }
    9. }
    10. public class Dog extends Animal{
    11. //傻狗 是狗的属性
    12. public boolean silly;
    13. public Dog(String name,int age,boolean silly) {
    14. //1. 先帮助父类部分初始化 必须放到第一行
    15. super(name,age);
    16. this.silly = silly;
    17. System.out.println("Dog(String ,int ,boolean )");
    18. }
    19. public static void main(String[] args) {
    20. Animal animal2 = new Dog("金毛",6,false);
    21. }
    22. }

    3.在子类构造方法中,super(…)调用父类构造时,必须是子类构造方法中第一条语句。
    4.super(…)只能在子类构造方法中出现一次,由与this(…)调用时也要在第一条语句,所以super(…)不能和this(…)同时出现,也就是是说子类构造方法中不能使用this(…)

    4. 避免在构造方法中调用重写的方法

    一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func

    1. class B {
    2. public B() {
    3. // do nothing
    4. func();
    5. }
    6. public void func() {
    7. System.out.println("B.func()");
    8. }
    9. }
    10. class D extends B {
    11. private int num = 1;
    12. @Override
    13. public void func() {
    14. System.out.println("D.func() " + num);
    15. }
    16. }
    17. public class Main {
    18. public static void main(String[] args) {
    19. D d = new D();
    20. }
    21. }

    执行结果:

    • 构造 D 对象的同时, 会调用 B 的构造方法.
    • B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
    • 此时 D 对象自身还没有构造, num 处在未初始化的状态, 值为 0;如果具备多态性,num的值应该是1.
    • 所以在构造函数内,尽量避免使用实例方法,除了final和private方法。
    【结论】:

    “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.

    二. 对象的默认初始化

    Java方法内部定义一个局部变量时,用户必须要将其赋值或者初始化,否则会编译失败;

    但对象中的字段(成员变量),用户不需要将其初始化就可直接访问使用,这里其原因在于new对象时,jvm会给出字段的默认初始化。

    下面是new对象是时,jvm层面执行的概述:

    1.检测对象对应的类是否加载了,如果没有加载则加载
    3.为对象分配内存空间
    3.处理并发安全问题
    比如:多个线程同时申请对象,JVM要保证给对象分配的空间不冲突
    4.初始化所分配的空间
    即:对象空间被申请好之后,对象中包含的成员已经设置好了初始值

    5.设置对象头信息(关于对象内存模型后面会介绍)
    6.调用构造方法,给对象中各个成员赋值

    三. 就地初始化对象

    在声明成员变量时,就直接给出了初始值。

    代码编译完成后,编译器会将所有给成员初始化的这些语句添加到各个构造方法中

    1. public class Date {
    2. public int year = 1900;
    3. public int month = 1;
    4. public int day = 1;
    5. public Date(){
    6. }
    7. public Date(int year, int month, int day) {
    8. }
    9. public static void main(String[] args) {
    10. Date d1 = new Date(2022,8,16);
    11. Date d2 = new Date();
    12. }
    13. }

    四. 类的初始化顺序

    1. 普通类(没有继承关系)

    1. 静态部分(静态变量、常量,静态代码块)
      • 在类加载阶段执行,类中存在多个静态部分时,会按顺序执行

    • 静态代码块只会执行一次,且静态的变量、常量等只会创建一份
    2.非静态部分(实例变量、常量、实例代码块)
    • 当有对象创建时才会执行,按顺序执行
    3.最后执行构造方法,当有对象创建时才会执行
    代码演示:

    1. class Person {
    2. public String name;
    3. public int age;
    4. public Organ organ = new Organ();
    5. public Person(String name, int age) {
    6. this.name = name;
    7. this.age = age;
    8. System.out.println("构造方法执行");
    9. }
    10. {
    11. System.out.println("实例代码块执行");
    12. }
    13. static {
    14. System.out.println("静态代码块执行");
    15. }
    16. }
    17. class Organ {
    18. //...
    19. public Organ() {
    20. System.out.println("实例变量::organ");
    21. }
    22. }
    23. public class TestDemo {
    24. public static void main(String[] args) {
    25. Person person1 = new Person("xin",21);
    26. System.out.println("==============");
    27. Person person2 = new Person("xinxin",20);
    28. }
    29. }

    执行结果:

    2. 派生类( 有继承关系)

    1.静态部分(静态变量、常量,静态代码块)
    • 父类静态代码块优先于子类静态代码块执行,且是最早执行
    • 只有第一次实例化子类对象时,父类和子类的静态部分会执行; 之后再实例化子类对象时,父类和子类的静态部分都不会再执行
    2.父类非静态部分(实例变量、常量、实例代码块)和父类构造方法
    3.子类非静态部分(实例变量、常量、实例代码块)和子类构造方法

    1. class Person {
    2. public String name;
    3. public int age;
    4. public Person(String name, int age) {
    5. this.name = name;
    6. this.age = age;
    7. System.out.println("Person:构造方法执行");
    8. }
    9. {
    10. System.out.println("Person:实例代码块执行");
    11. }
    12. static {
    13. System.out.println("Person:静态代码块执行");
    14. }
    15. }
    16. class Student extends Person{
    17. public Student(String name,int age) {
    18. super(name,age);
    19. System.out.println("Student:构造方法执行");
    20. }
    21. {
    22. System.out.println("Student:实例代码块执行");
    23. }
    24. static {
    25. System.out.println("Student:静态代码块执行");
    26. }
    27. }
    28. public class TestDemo4 {
    29. public static void main(String[] args) {
    30. Student student1 = new Student("张三",19);
    31. System.out.println("===========================");
    32. Student student2 = new Student("gaobo",20);
    33. }
    34. public static void main1(String[] args) {
    35. Person person1 = new Person("bit",10);
    36. System.out.println("============================");
    37. Person person2 = new Person("gaobo",20);
    38. }
    39. }

    执行结果:

  • 相关阅读:
    Docker安装Yapi
    【刷题心得】双指针法|HashSet<T>
    「MySQL高级篇」MySQL之MVCC实现原理&&事务隔离级别的实现
    Redis从入门到放弃(1):安装配置
    mybatisplus Lambda函数转属性名
    闭包和装饰器
    Python 迭代器与生成器
    建筑楼宇VR火灾扑灭救援虚拟仿真软件厂家
    银行数字化的两难:安全还是效率?
    华为HCIP Datacom H12-821 卷19
  • 原文地址:https://blog.csdn.net/jcc4261/article/details/127957792