系列文章目录
一:上手
二:类和对象
三:primitive主数据类型和引用
四:方法操作实例变量
五:编写程序
六:继承、接口与多态
七:构造器与垃圾收集器
文章目录
前言
行百里者半九十
对象有生有死,因此要为对象的生命循环周期负责,决定着对象何时创建、如何创建,何时销毁对象。
1、栈与堆:生存空间
在我们能够弄清楚java创建对象的真正发生过程之前,我们需要退回一步,先对生存在java中的事务更加了解,这也就表示我们要对栈与堆有着更多的认识。
在java中,我们会更加注重内存中的两种区域,一个是对象的生存空间堆(heap),一个是方法调用及变量的生成空间栈(stack)。
当java虚拟机启动时,它会从底层的操作系统中取得一块内存,并以此区域来执行java程序。而且我们要知道,所有的对象都存活与可垃圾回收的堆上。但是我们却没有绝对的定义过变量存货的区域。那是因为变量分为实例变量和局部变量,实例变量存在于所属的对象中,也就是说它存活于堆上,而局部变量的和方法的参数都是声明在方法中,是暂时的,所以他们生命周期只限于这个方法的执行时间,所以这种区域变量,又被称为栈变量,所以它存活于栈中。
实例变量
实例变量是被声明在类而不是方法中的,他们代表每个独立对象的“字段”,每个实例都能有不同的值,实例变量存在于所属的对象中:
public class Duck {
int size;//每个Duck都会有独立的size
}
局部变量
局部变量和方法的参数都是被声明在方法中的,他们是暂时的,且生命周期只限于方法被放在栈上的这段期间,也就是方法调用至执行完毕为止。
public void foo(int x) {
int i = x + 3;
boolean b = true;
}
方法会被堆在一起
当你调用一个方法时,这个方法会被放在调用栈的栈顶,如果这个方法继续调用别的方法的话,那么被调用的新的方法就会被放在这个栈的栈顶,压着刚才那个方法,也就形成了一个栈块,而栈块的执行顺序就是由上到下。被放上栈的其实是堆栈块,它带有方法的状态,包括执行到哪一行程序以及所有的局部变量的值。
2、栈上的对象引用
非primitive的变量只是保存对象的引用,而不是对象本身,对象本身只会存在于堆上。
public class StackRef{
public void foof(){
barf();
}
public void barf(){
Duck d=new Duck(24);
}
}
比如上面程序中先声明了foof,将他置在栈上,然后调用了braf方法,至于栈顶,构建了Duck对象的引用变量d,此时的d就在堆上,不管对象是在哪里声明的,他总是运行在堆上。
如果局部变量生存在栈上,那么实例变量呢?
实例变量就是存在于对象所属的堆空间上。
记住对象的实例变量的值,是存在对象中。如果实例变量都是primitive主数据类型的,则java会根据类型大小分配空间,int需要32,但不论int的值多大,它都是32位。所以java并不在乎类型的值,只在乎类型。
举个例子,现在有一个类,里面有一个实例变量,实例变量的类型是另一个类的类型,那么我们分析一下它们在堆上的联系。
public class CellPhone(){
private Antenna ant; //声明引用对象,没有赋值
priavte Antenna antt=new Antenna(); //声明并赋值
}
如果有声明变量但没有给它赋值,则只会留下变量的空间
private Antenna ant;
直到引用变量赋值一个新的Antenna对象才会在堆上占有空间:
private Antenna ant = new Antenna();
3、构造函数
回顾一下创建对象的步骤:声明、创建、赋值Duck myDuck = new Duck();
。这里的Duck()看起来很像是在调用Duck()方法,但其实并不是。我们是在调用Duck的构造函数。
构造函数看起来很像方法,但他并不是方法,它带有new的时候会执行的代码程序,换句话说,这段程序代码会在你初始一个对象的时候执行。就算你没有自己写构造函数,编译器也会帮你写一个。
唯一能够调用构造函数的办法就是新建一个类。
程序举例
下面我们构造一个Duck,让我们更好的介入new过程
class Duck {
//构造函数在此
public Duck() {
System.out.println("gagaga");
}
}
public class UseDuck {
public static void main(String[] args) {
Duck d = new Duck();//在这里会启动Duck的构造函数
}
}
注意有什么不同,方法有返回类型,构造函数没有返回类型。
运行结果:
如果某种对象不应该在状态被初始化之前就使用,就别让任何人能在不初始化的情况下取得该对象!让用户先构造出Duck对象在设定状态(比如大小)很危险,如果用户不知道或者忘记执行setSize()怎么办?
最好的办法就是把初始化的程序放在构造函数中,然后把构造函数设置成需要参数的
class Duck {
int size;
//构造函数在此
public Duck(int DuckSize) {
System.out.println("gagaga");
size = DuckSize;
System.out.println("Size is " + size);
}
}
public class UseDuck {
public static void main(String[] args) {
Duck d = new Duck(24);//在这里会启动Duck的构造函数
}
}
但上面的程序不传入参数时会报错,因此一个更具有鲁棒性的写法就是
class Duck {
int size;
//构造函数在此
public Duck() {
size =27;//指定默认值
System.out.println("gagaga");
System.out.println("Size is " + size);
}
public Duck(int DuckSize) {
System.out.println("gagaga");
size = DuckSize;//使用参数设置
System.out.println("Size is " + size);
}
}
public class UseDuck {
public static void main(String[] args) {
//不知道大小时
Duck d1 = new Duck();//在这里会启动Duck的构造函数
//知道大小时
Duck d2 = new Duck(48);
}
}
执行结果
这里就是构造函数的重载
重载构造函数
重载构造函数的意思代表你有一个以上的构造函数且参数都不相同
public class TestDuck {
public static void main(String[] args) {
int weight = 8;
float density = 2.3F;
String name = "Donald";
long[] feathers = {1,2,3,4,5,6};
boolean canFly = true;
int airspeed = 22;
Duck[] d = new Duck[7];
d[0] = new Duck();
d[1] = new Duck(density, weight);
d[2] = new Duck(name, feathers);
d[3] = new Duck(canFly);
d[4] = new Duck(3.3F, airspeed);
d[5] = new Duck(false);
d[6] = new Duck(airspeed, density);
}
}
class Duck {
int pounds = 6;
float floatability = 2.1F;
String name = "Generic";
long[] feathers = {1,2,3,4,5,6,7};
boolean canFly = true;
int maxSpeed = 25;
public Duck() {
System.out.println("type 1 duck");
}
public Duck(boolean fly) {
canFly = fly;
System.out.println("type 2 duck");
}
public Duck(String n, long[] f) {
name = n;
feathers = f;
System.out.println("type 3 duck");
}
public Duck(int w, float f) {
pounds = w;
floatability = f;
System.out.println("type 4 duck");
}
public Duck(float density, int max) {
floatability = density;
maxSpeed = max;
System.out.println("type 5 duck");
}
}
如果你已经写了一个有参数的构造函数,并且需要一个没有参数的构造函数,则必须要自己写。此时的编译器无法帮你生成无参的构造函数。
父类的构造函数在对象的生命中所扮演的角色
在创建新对象时,所有继承下来的构造函数都会执行
这代表每个父类都有一个构造函数(因为每个类都至少有一个构造函数),且每个构造函数都会在子类对象创建时期执行。执行new的指令是个重大事件,他会启动构造函数的连锁反应。还有,就算是抽象的类也有构造函数,虽然不能对抽象的类执行new操作,但是抽象的类还是父类,因此它的构造函数会在具体的子类创建出实例时执行。
如何调用父类的构造函数?
调用父类构造函数的唯一方法是调用super()
abstract class Animal {
private String name;
public String getName() {//Hippo会继承这个getter方法
return name;
}
public Animal(String theName) {//有参数的构造函数,用来设定name
name = theName;
}
}
class Hippo extends Animal {
public Hippo(String name) {
super(name);//传给Animal的构造函数
}
}
public class MakeHippo {
public static void main(String[] args) {
Hippo h = new Hippo("Buffy");
System.out.println(h.getName());
}
}
执行结果
注意,父类的部分必须在子类创建完成之前就必须完整的成型,子类对象可能会动用到从父类继承下来的东西,所以父类的构造函数一定要再子类的构造函数之前完成。
从某个构造函数中调用重载版的另一个构造函数
- 使用this()来从某个构造函数调用同一个类的另一个构造函数
- this()只能用在构造函数中,且必须是第一行语句
- super()与this()不能兼得
比如比较年龄大小这个示例程序
class Person{
int id;
String name;
int age;
//有参构造函数
public Person(int id,String name ,int age){
this.id = id;
this.name = name;
this.age = age;
}
//比较年龄的方法,这是本身就具备的,还有一个和谁比较是未知数
public void compareAge(Person p2){
if(this.age>p2.age){
System.out.println(this.name+"大!");
}else if(this.age<p2.age){
System.out.println(p2.name+"大!");
}else{
System.out.println("同龄");
}
}
}
public class comparePerson{
public static void main(String[] args) {
Person p1 = new Person(110,"狗娃",17);
Person p2 = new Person(119,"铁蛋",9);
p1.compareAge(p2);
}
}
4、对象的生命周期
- 局部变量只会存活在声明该变量的方法中
- 实例变量的寿命与对象相同,如果对象还活着,则实例变量也是活的
- 除非有对对象的引用,否则该对象一点意义也没有
三种方法可以释放对象的引用
1、引用永久性离开它的范围
void go() {
Life z = new Life();
}
z会在方法结束时消失
2、引用被赋值到其他的对象上
Life z = new Life();
z = new Life();
第一个对象会在z被赋值到别处时挂掉
3、直接将引用设定为null
Life z = new Life();
z = null;
第一个对象会在z被赋值为null时死掉。
null的理解
当你把引用设置为null时,你就等于抹除了遥控器的功能,换句话说,你拿了一个没有电视的遥控器。
你期待喵喵叫的这个动作,但是没有Cat可以执行!
5、总结
- 我们关心栈与堆这两种内存空间
- 实例变量声明在类中方法之外的地方
- 局部变量声明在方法或者方法的参数上
- 所有局部变量都存在与栈上相对应的堆栈块中
- 对象引用变量与primitive主数据类型变量都是放在栈上
- 不管是实例变量还是局部变量,对象本身都在堆上
- 实例变量保存在所属的对象中,位于堆上
- 如果实例变量是个对对象的引用,则引用与对象都在堆上
- 构造函数是在新建对象时执行的程序代码
- 构造函数必须与类名同名且没有返回类型
- 你可以用构造函数来初始化被创建的对象的状态
- 如果没写构造函数,编译器会帮你安排一个
- 默认的构造函数是没有参数的
- 最好能有无参数的构造函数让人选择使用默认参数
- 重载的构造函数意思是有超过一个以上的构造函数,且有不同的参数
- 两个构造函数的参数必须不同
- 实例变量有默认值,原始的默认值是0、0.0、false,引用变量的默认值是null