一、构造函数概述与默认构造函数
那么什么是构造函数呢?
既然是函数,那它应该是被定义在类中,同时有函数名,要明确函数返回的结果和参数类型。
我们这里先看一下构造函数的特点:
1.函数名与类名相同;
2.不用定义返回值类型
3.没有具体的返回值。
从这些特点来说,这确实是一个很特殊的函数哦。
我们来看一个定义构造函数的例子:
class Person
{
private String name;
private int age;
//定义一个Person类的构造函数
Person()//构造函数,而且是空参数的。
{
System.out.println("person run");
}
public void speak()
{
System.out.println(name+":"+age);
}
}
//测试类
class ConsDemo
{
public static void main(String[] args)
{
Person p = new Person();
}
}
结果:
那么我们可以这样通俗的定义构造函数:
构造函数:构建创造对象时调用的函数。这样是不是很好理解了。
那么我们就可以清楚的知道构造函数的主要作用:
构造函数的作用:可以给对象进行初始化。
我们必须清楚,创建的每一个对象都必须要通过构造函数初始化。
那么当我们一个类中没有构造函数时,为什么我们还是可以创建对象呢?那是因为有默认构造函数。
什么是默认构造函数呢?
当一个类中如果没有定义过构造函数,那么该类中会有一个默认的空参数构造函数。
class Person(){}//构造函数,而且是空参数的,这其实就是类中的默认构造函数
但我们更要注意的是:如果在类中定义了指定的构造函数,那么类中的默认构造函数就没有了。
比如说我们定义一个类:
class Demo{}//一个什么代码都没有的类
我们说这个类中有没有内容?
答案当然是肯定的,虽然这个类是空的,但是我们可以创建它的对象,也就是说这个类中有一个默认的构造函数:
class Demo
{
Demo(){}
}
很显然,这个类中确实有内容,就是我们说的默认构造函数。
二、构造函数与一般函数的区别
这一节我们简单学习一下构造函数与一般函数之间的区别所在。
那么它们有什么区别呢,结合上一节,我们可以总结出以下两点区别:
第一个区别:
构造函数:对象创建时,就会调用与之对应的构造函数,对对象进行初始化。
一般函数:对象创建后,需要函数功能时,才调用函数。
这也就是说创建对象时,构造函数一定被调用,而一般函数不一定被调用。
第二个区别:
构造函数:对象创建时,会调用且只调用一次。
一般函数:对象创建后,可以被调用多次。
简单看看示例:
class Person
{
private String name;
private int age;
Person()
{
System.out.println("person run");
}
public void speak()
{
System.out.println(name+":"+age);
}
}
class ConsDemo
{
public static void main(String[] args)
{
Person p = new Person();//此时调用了构造函数
p.speak();//调用两次speak方法
p.speak();
}
}
我们可以看到当创建一个对象p是,调用了Person类中的构造函数,而且是只调用了一次。而对于一般方法speak方法,是我们在创建了对象之后,才调用,并且我们可以调用任意次娄。
三、构造函数的重载
要使一个Person对象一出生就有名字,那么我们可以定义如下一个构造函数:
class Person
{
private String name;
private int age;
Person()//构造函数,分别为成员变量name和age赋固定的值
{
name = "baby";
age = 1;
System.out.println("person run");
}
Person(String n)//构造函数,有一个初始name参数
{
name = n;
}
public void speak()
{
System.out.println(name+":"+age);
}
}
我们来测试一下:
class ConsDemo
{
public static void main(String[] args)
{
Person p = new Person();
p.speak();
Person p1 = new Person("旺财");
p1.speak();
}
}
看一下结果:
同理,我们可以定义另外一个构造函数,把name和age两个参数同时初始化
class Person
{
private String name;
private int age;
Person()//构造函数,分别为成员变量name和age赋固定的值
{
name = "baby";
age = 1;
System.out.println("person run");
}
Person(String n)//构造函数,有一个初始name参数
{
name = n;
}
Person(String n,int a)
{
name = n;
age = a;
}
public void speak()
{
System.out.println(name+":"+age);
}
}
测试:
class ConsDemo
{
public static void main(String[] args)
{
Person p = new Person();
p.speak();
Person p1 = new Person("旺财");
p1.speak();
Person p2 = new Person("小强",10);
p2.speak();
}
}
结果:
从上面我们看到,一个类中我们定义了多个构造函数,它们的参数各不相同,这种现象就是构造函数的重载。
通过构造函数的重载,我们可以通过不同的构造函数初始化不同的对象。
四、构造函数的内存加载
我们之前说过,构造函数只有创建对象时才会被调用并且只调用一次。那么在创建对象的过程中内存中的变化是什么样的呢?
我们接着上一节的Person类,我们分析
class Person
{
private String name;
private int age;
Person()//构造函数,分别为成员变量name和age赋固定的值
{
name = "baby";
age = 1;
System.out.println("person run");
}
Person(String n)//构造函数,有一个初始name参数
{
name = n;
}
Person(String n,int a)
{
name = n;
age = a;
}
public void speak()
{
System.out.println(name+":"+age);
}
}
我们用下面的两行代码来分析一下构造函数在内存中的加载过程
class ConsDemo
{
public static void main(String[] args)
{
Person p = new Person("小强",10);
p.speak();
}
}
对于上面的测试,我们分析它的运行过程:
1.main方法进栈内存,main方法中有一个Person类类型变量p;2.new创建Person对象,在堆内存中创建空间(假如地址为0x0045),该空间中有两个成员变量name和age;
3.对对象的两个成员变量进行初始化,此时会自动选择调用构造函数Person(String n,int a);
4.构造函数Person(String n,int a)进栈内存,并且有参数n="小强",a=0;
5.然后在堆内存中把参数n和a的数值初始化name和age变量,此时对象的初始化完成;
6.把地址0x0045赋给main方法中的变量p;
7.构造函数Person()出栈,释放参数n和a;
8.执行p.speak()语句,调用Person类中的speak()方法,则speak方法进栈;
9.执行打印语句,跳出speak方法,speak方法出栈;
10.跳出main方法,main方法出栈,程序运行结束。
我们在上面通过对一个简单的对象创建过程进行了分析,简单的学习了构造函数在内存中的加载和运行过程,这里也就是突出了对象的初始化,如果类中没有定义构造函数,那么我们在创建对象时会调用默认的构造函数,而当我们定义了构造函数,则会通过参数类型选择不同的构造函数进行对象的初始化,而且我们知道对象都必须被初始化,初始化就会调用相应的构造函数,所以说,构造函数是必须会进栈内存的。
五、构造函数需要注意的几个细节
那么我们在定义和调用构造函数时,需要注意哪些细节呢?
第一个需要注意的细节:构造函数与set方法
看两个函数代码:
class Person { private String name; private int age; //构造函数,初始化name Person(String n) { name = n; System.out.println(name+"age"+age); } //一般函数,设置name public void setName(String n) { name = n; } }
我们看到上面有两个函数,第一个是构造函数,第二个是一般方法,它们两个都是为设置name的内容,那么我们能不能用构造函数代替set方法呢,很显然,这是不行了,因为我们之前已经学习了构造函数和一般函数的区别,而set方法就是一个一般函数。构造函数在这里只是对name进行了一次初始化,之后就不再作用了,而set方法当我们需要更改名字的任何时候都可以调用。
第二个需要注意的细节:构造函数可以调用一般函数,但是一般函数不可以直接调用构造函数。
我们来看例子:
class Person { private String name; private int age; //构造函数,初始化name Person(String n) { setName(n); System.out.println(name+":"+age); } //一般函数,设置name public void setName(String n) { name = n; } }
编译通过,我们来测试一下
class PersonTest { public static void main(String[] args) { Person p = new Person("小强"); } }
结果:
很显然构造函数中调用一般函数是可以的。
当我们在set方法中调用构造函数:
class Person { private String name; private int age; //构造函数,初始化name Person(String n) { name = n; System.out.println(name+":"+age); } //一般函数,设置name public void setName(String n) { Person(n); name = n; } }
结果:
我们看到编译直接通不过,所以说一般函数中是不能直接调用构造函数的。
第三个需要注意的细节:与类名同名的一般方法
我们看这样一个函数:
class Person { private String name; private int age; void Person(String n) { name = n; System.out.println(name+":"+age); } }
测试:
class PersonTest { public static void main(String[] args) { Person p = new Person("小强"); } }
结果:
我们看到提示说无法将构造函数应用到给定类型,实际没有参数,但我们的代码中有参数“小强”,也就是说我们创建对象用的函数不是构造函数,而是一个一般函数,程序没有在Person类中找到相应的构造函数,因为我们的代码中没有定义构造函数,那么就只有默认构造函数,而默认构造函数是没有任何参数的。所以我们在以后的编程过程中一定要注意这种现象。
第四个需要注意的细节:构造函数中有return语句。
我们看代码说明问题
class Person { private String name; private int age; Person(String n,int a) { if(a<0) { System.out.println("初始化不合法!"); return; } name = n; age = a System.out.println(name+":"+age); return ;//我们在函数那里学习了,没有返回值的函数中的return语句是可以省略的 } }
我们来测试一下
class PersonTest { public static void main(String[] args) { Person p = new Person("小强",-1); } }
结果:
我们发现了程序编译通过并且运行正常,return语句跳出构造函数,那么也就是说构造函数中有return语句。
这一节我们学习了四个构造函数中经常需要注意的细节,在以后的编程过程中我们多多注意。
六、this关键字的原理
class Person
{
private String name;
private int age;
Person(String name,int age)
{
this.name = name;
this.age = age;
}
public void speak()
{
System.out.println(name+":"+age);
}
}
class ThisTest
{
public static void main(String[] args)
{
Person kobe = new Person("KOBE",37);
kobe.speak();
}
}
结果:
所以我们可以说,当成员变量与局部变量重名时,我们可以用this来区分。
那么我们就想明白,this到底代表什么呢?java语言说,this代表的是对象。我们还想明白,this代表的是哪个对象?java语言又说,代表的就是当前对象。
专业点的术语是这样定义this的:this就是所在函数所在对象的引用。说简单点就是:this代表本类对象的引用。
我们自己用通俗点的语言来定义:就是哪个对象调用了this所在的哪个函数,this就代表哪个对象,也就是说this就是这个对象的引用。
比如上面的例子中的kobe调用了构造函数Person(String name,int age),那么我们可说this就可以代表kobe这个对象。
那么我们再来对this在内存中的体现过程分析一下,我们继续7.4的过程,只有小的变动。
1.main方法进栈内存,main方法中有一个Person类类型变量kobe;
2.new创建Person对象,在堆内存中创建空间(假如地址为0x0045),该空间中有两个成员变量name和age;
3.对对象的两个成员变量进行初始化,此时会自动选择调用构造函数Person(String n,int a);
4.构造函数Person(String name,int age)进栈内存,参数name="KOBE",age=0也加载入栈。而此时系统会自动为该栈内存中加载一个对象的引用,也就是this,并且把kobe的堆内存地址赋给this;
5.然后在把this.name和this.age的初始化为栈内存中name和age,这样就很清晰了,this.name和this.age我们可以理解为就this所指堆内存中对象的成员变量,此时对象的初始化完成;
6.把地址0x0045赋给main方法中的实例变量kobe;
7.构造函数Person(String name,int age)出栈,释放参数name和age和this引用;
8.执行kobe.speak()语句,调用Person类中的speak()方法,则speak方法进栈,此时系统也会为speak方法加载一个this引用,指向堆内存中的对象地址(0x0045);
9.执行打印语句,跳出speak方法,speak方法出栈,释放this引用;
10.跳出main方法,main方法出栈,程序运行结束。
通过上面的过程分析,我们可以简单的总结出这样一个结论:当在函数中需要用到调用函数的对象时,就用this关键字。
为了更好的理解这个结论,我们把上面的例子可以标准的写成下面这样:
class Person
{
private String name;
private int age;
Person(String name,int age)
{
this.name = name;
this.age = age;
}
public void speak()
{
System.out.println(this.name+":"+this.age);
}
}
我们对这个类方法中的所有成员变量都标准的写成了this.成员变量的格式。
那么我们刚开始看的第一个例子为什么没有this,而结果是也是正确的呢?
很显然,当然对于成员变量与局部变量不重名时,this是可以省略的,但请注意,不是没有,而是省略。
七、this关键字的细节与应用
那么构造函数与构造函数之间是怎么访问呢?
为了掌握这个问题,我们必须要明确一个概念,那就是在一个类中的成员如果要被执行,就必须由对象调用.而每个对象都有一个所属的this.
java语言给了关键字this这个功能,那就是用this调用构造函数,而且也是通过参数不同选择调用不同的构造函数.
我们来看一个例子:
class Person
{
private String name;
private int age;
Person()//构造方法1
{
System.out.println("person run");
}
Person(String name)//构造方法2
{
this.name = name;
}
Person(String name,int age)//构造方法3
{
this.name = name;
this.age = age;
}
public void speak()
{
System.out.println(this.name+":"+this.age);
}
}
class ThisTest
{
public static void main(String[] args)
{
Person kobe = new Person("科比",37);
kobe.speak();
}
}
对于上面的例子我们可以看到,构造函数3中的第一句语句其实我们已经通过构造函数2实现了,那么为了提高代码的复用性,我们为什么不调用构造函数2呢,java语言中用下面的语句来调用构造函数:
Person(String name,int age)//构造方法3
{
this(name);//用this关键字调用了构造函数Person(String name)
this.age = age;
}
我们看到了this关键字可以用于在构造函数中调用其他构造函数,当然对于调用那个构造函数,还是通过参数来确定.
那么我们就明确了构造函数与构造函数之间的调用形式.
下面我们来看两个this关键字用法中需要注意的两个细节:
第一个细节:构造函数中调用构造函数只能定义在构造函数的第一行.
这是为什么呢,因为初始化动作一定要先执行.这就是java语言定义的一个规则,如果不是定义在第一行,编译直接通不过.
我们看例子,把上面的构造函数3的语句交换位置:
Person(String name,int age)//构造方法3
{
this.age = age;
this(name);//用this关键字调用了构造函数Person(String name)
}
我们看编译情况:
我们看到编译错误:对this的调用必须是构造器中的第一个语句,这就是我们在使用this关键字时的第一个细节.
第二个细节:注意构造函数的调用出现递归循环而导致栈内在溢出.
我们看个例子:
Person()//构造方法1
{
this("KOBE");
System.out.println("person run");
}
Person(String name)//构造方法2
{
this();
this.name = name;
}
结果:
我们很容易发现两个构造函数相互调用,形成了递归,使得两个构造函数不断进栈,最后栈内存溢出,程序终止.
上面我们看了两个在使用this关键字时需要注意的细节,下面我们再简单的看看this关键的应用情况.
我们一般什么时候使用this呢,我们上一节中说过this的概念:this就代表对象,就是所在函数所在对象的引用.那么我们不难理解,当我们在一个类中用到了本类的对象,我们就通常会用this来引用.
那么我们来实现一个功能:判断两个人是否是同龄人.
分析一下:要判断两个人是否同龄,我们只需要比较这两个人(也就是两个Person对象)的年龄是否相等即可,那么也就是说一个对象可以直接调用Person类中的方法来与另一个对象做比较就可以了.
我们可以这样实现:
//判断两个人是否同龄
public boolean compare(Person p)
{
return this.age == p.age;
}
我们来测试一下:
class ThisTest
{
public static void main(String[] args)
{
Person kobe = new Person("科比",37);
Person james = new Person("詹姆斯",31);
kobe.speak();
james.speak();
System.out.println("这两个人相等吗:"+kobe.compare(james));
}
}
结果:
我们看到this的主要应用就是代表对象,那个对象调用了this所有的函数,我们就通俗的认为this就代表那个对象.