前言
首先由一串代码引出今天的问题:
public class Student {
String name;
String gender;
int age;
public void setStudentInfo(String name,String gender,int age){
this.name=name;
this.gender=gender;
this.age=age;
}
public void printStudentInfo(){
System.out.println(name+","+gender+","+age);
}
public static void main(String[] args) {
Student s1=new Student();
s1.setStudentInfo("LiMing","男",13);
s1.printStudentInfo();
Student s2=new Student();
s2.setStudentInfo("Jenny","女",12);
s2.printStudentInfo();
}
}
上述代码创建了一个学生类,并且在主函数中利用类初始化了两个对象s1和s2,将对象创建好了之后才设置的数据,这样我们就会发现问题:过程有些繁琐了。我们期望达到的目的是:对象在创建好之后能否将数据设置进去。
那么我们可不可以下面这样呢?
Student s3=new Student("Danny","男",14);
敲完之后会发现会飘红,编译发现不能完成:显示错误为:
其实针对这条语句:
Student s1=new Student();
内部是很复杂的:
1.从堆上开辟对象大小的内存空间。
2.对空间来进行初始化,将其构造成一个真正的对象。
这里就引出了构造方法,也称为构造器。
一、概念
构造方法是一个特殊的成员方法,名字必须与类名相同,在创建对象时,由编译器自动调用,并且在整个对象的生命周期内只调用一次。
那么下面我们在程序中加上下面这段代码:
public void Student(){
System.out.println("我是构造方法");
}
会发现程序编译可以通过,但是不能执行,也就是打印不出“我是构造方法“这条语句,这里加上”void“,编译器不会认为这是构造方法,因为在创建对象时,该方法没有执行,删掉void,运行成功。
二、特性
1.名字与类型相同,没有返回值,设置为void也不行。
2.一般情况下使用public修饰。
3.在创建对象时由编译器自动调用,并且在对象的生命周期内只调用一次(对象的出生其实类似于人的出生,我们每个人也只能出生一次,在对象出生的时候调用构造方法,所以自然是只能调用一次)。
注意: 构造方法的作用就是对对象中的成员进行初始化,并不负责给对象开辟空间。
那么回到问题的开始,怎样才能创建好s3呢?我们想要传进三个参数,但是程序的构造方法中并没有三个参数,于是创建:
public Student(String name,String gender,int age){
this.name=name;
this.gender=gender;
this.age=age;
}
之后在s1处打一个断点,程序执行到这里按F7,因为这里创建s1时是无参的,所以进入无参的构造函数,假如是s3,就会进入上述的构造函数。此时就不需要再调用setStudentInfo了,很方便。
同时这两个构造方法一个是无参,一个是有参,就构成了方法重载。
这是就出现了构造方法的第四个特性:
4.构造方法可以重载(用户根据自己的需求提供不同参数的构造方法)。
5.如果用户没有显式定义,编译器会生成一份默认的构造方法,生成的默认构造方法一定是无参的。(显式:用户自己实现)
怎么证明?
首先先在Student类中打开jclasslib,这里我们定义了两个构造方法,且构造方法中一定有一个隐藏的参数this。
之后打开日期类,这个类中我没有定义构造方法,但是会发现里边也有一个构造方法,里面的参数只有一个this。这就是编译器生成的默认的构造方法。证明特性5
假如我们给Date类加上一个含有一个参数的构造方法,代码如下:
public class Date {
public int year;
public int month;
public int day;
//带有一个参数的构造方法写好了,注意:实际这个方法中含有两个参数,还有一个隐藏的this
public Date(int year){
this.year=year;
}
public void setDay(int y,int m,int d){
year=y;
month=m;
day=d;
}
public void printDate(){
System.out.println(year+"-"+month+"-"+day);
}
public static void main(String[] args){
Date d=new Date(2021);
d.setDay(2021,9,13);
d.printDate();
}
}
那么我们打开jclasslib来看:
会发现只含有一个构造方法,所以在用户显式定义之后,编译器就不会生成默认的构造方法。
6.构造方法中,可以通过this调用其他构造方法来简化代码,可以减少重复代码的出现。
证明:
public class Date {
public int year;
public int month;
public int day;
//带有一个参数的构造方法写好了,注意:实际这个方法中含有两个参数,还有一个隐藏的this
public Date(int year){
this.year=year;
System.out.println("在构造时已经将year初始化好了");
}
public Date(int year,int month){
this(year);//调用带有year参数的构造方法
//Date(2021);//编译出错,不能按照该种方式进行调用
this.month=month;
}
public void printDate(){
System.out.println(year+"-"+month+"-"+day);
}
public static void main(String[] args){
Date d1=new Date(2021);
Date d2=new Date(2021,9);
}
}
在此处打一个断点(Debug模式):
按F7会发现进入带有一个参数的构造方法。将方法执行完进入到d2,F7会进入含有两个参数的构造方法,执行到this(year)之后F7就会进入到含有一个参数的构造方法中。第六个特性得以证明。
但是this(year)这条语句是有限制条件的:这条语句必须是构造方法中的第一条语句。
会发现添加下述代码,此条语句就会飘红,编译报错。
注意:不要调用成环。 如下图:
编译报错:
假如我们实例化一个对象,Date d = new Date(2021);
首先开辟空间,之后调用构造方法,那么会先进入一个参数的方法,在方法中进入两个参数的方法,之后又进入一个参数的。于是形成无限递归。
7.绝大多数情况下使用public来修饰,特殊场景下会被private修饰。
一定一定要注意:构造方法只负责初始化对象,不负责给对象开辟内存空间。
三、默认初始化
前面我们提到:在主函数中,如果定义一个变量,必须给它初始化,不然会报错,那么在Date类中,开头这三个基本类型的成员变量,我们并没有给他设置初始值,为什么能成功运行?
这里我们新创建一个Person类
插播一个快捷键:当我们定义好成员变量想要写构造方法时,可以让IDEA自动替我们生成,有些电脑可以alt+insert,我的电脑不可以,但是鼠标右键可以出来:
就会自动生成。
插播结束。
新建一个Person类:
public class Person {
int age;
public Person(int age){
System.out.println(this.age);
}
//还可以让IDEA自动替用户生成
public void printPersonInfo(){
System.out.println(age);
}
public static void main(String[] args){
//构造方法如果用户没有显式实现,则编译器会自动生成一份,Person(){}
int a=10;
System.out.println(a);
//在方法体中的局部变量在使用时必须要初始化
//int b;
//System.out.println(b);
Person p=new Person(2021);
p.printPersonInfo();
}
}
在上述程序中,我们会发现构造方法中并没有给age赋值,而是直接打印。
结果为
其中第一个0是对p开辟内存空间后调用构造方法,而构造方法中没有给age赋值,直接打印它的默认值也就是0,第二个0就是printPersonInfo方法打印。
所以成员变量和局部变量是不一样的。
要搞清楚这个过程,就需要知道new关键字背后所发生的一些事情,在程序层面只是简单的一条语句,在JVM层面需要做好多事情,下面简单介绍下:
1.检测对象对应的类是否加载了,如果没有加载则加载。
2.为对象分配内存空间。
3.处理并发安全问题,比如:多个线程同时申请对象,JVM要保证给对象分配的空间不冲突。
4.初始化所分配的空间
即:对象空间被申请好之后,对象中包含的成员已经设置好了初始值,比如:
5.设置对象头信息。
以日期类举例:
6.调用构造方法,给对象中各个成员赋值。
四、就地初始化
public class Date6 {
//就地初始化,在定义成员变量时就设置好了默认值
int year=2021;
int month=9;
int day=16;
public void printDate(){
System.out.println(year+"-"+month+"-"+day);
}
public static void main(String[] args){
Date6 d=new Date6();
d.printDate();
}
}
结果:
那么假如我们再给一个d2,他的打印结果是什么呢?
public class Date6 {
//就地初始化,在定义成员变量时就设置好了默认值
int year=2021;
int month=9;
int day=16;
public Date6(int year,int month,int day){
}
public Date6(){
}
public void printDate(){
System.out.println(year+"-"+month+"-"+day);
}
public static void main(String[] args){
Date6 d1=new Date6();
d1.printDate();
Date6 d2=new Date6(2006,6,23);
d2.printDate();
}
}
结果:
成员变量使用了就地初始化,编译器在编译代码期间会将就地初始化的内容拷贝到每个构造方法中,并且在用户所写的语句之前。 什么意思呢?
public class Date6 {
int year=2021;
int month=9;
int day=16;
public Date6(int year,int month,int day){
System.out.println(this.year+"-"+this.month+"-"+this.day);
this.year=year;
this.month=month;
this.day=day;
System.out.println(this.year+"-"+this.month+"-"+this.day);
}
public Date6(){
}
public void printDate(){
System.out.println(year+"-"+month+"-"+day);
}
public static void main(String[] args){
Date6 d2=new Date6(2006,6,23);
d2.printDate();
}
}
运行结果:
针对结果进行解释:首先第一个是因为一开始编译器已经将就地初始化的代码拷贝进来,此时还没有赋值,所以直接打印就地初始化的内容,后面已经赋值,相当于把上面的已经修改,打印的就是修改后的内容了。
也就是说:构造方法执行时,先执行就地初始化的代码,然后再执行构造方法中的语句。