写在前面:
该学习笔记基于《Head First Java》一书,仅供个人参考备忘使用,可能会存在诸多问题,也会随着学习的推进不断修改,因此请酌情将其当作参考。
目录
类的构造函数
之前我们看到的类大多是这个样子的:
class Animal {
private String name;
public void setName(String newName) {
name = newName;
}
}
其中有实例变量,也有方法,但并没有提到“构造函数”这一概念。
再看下面这条创建对象的语句:
Animal creature = new Animal();
在该行语句中出现了Animal()这样一种表述,会让人理所当然地认为是在调用一个方法。实际上,这种“类名()”形式即为类的构造函数。
类的构造函数形式框架如下:
class className {
public className(paramters) {
statements;
}
}
构造函数有两个特征:
1.构造函数必须与方法同名;
2.构造函数一定不能有返回值。
构造函数的主要用途是为某些实例变量赋上初值。通过指定构造函数的参数列表,可以使得用户在使用构造函数创建对象时,就可以为对象的某些属性指定想要的值。
构造函数的重载:
之前在谈函数重载时,曾经说过重载的一个主要用途即是对构造函数的重载。为类设置多个具有不同参数列表的构造函数,可以确保用户在不同的初始条件下都能够通过构造函数来创建对象(例如当创建一个代表Car的对象时,可能并不知道车辆的行驶里程等详细信息,但由于Car类拥有形式丰富的构造函数,也许可以仅提供车辆的品牌即可创建一个Car对象)。
因此,在加入了重载之后,一个类的构造函数便有三个特征:
1.构造函数必须与方法同名;
2.构造函数一定不能有返回值;
3.不同的构造函数,参数列表必须不同。
private的构造函数?
构造函数并不一定要是public的,有时根据一些特殊的需求,可以将一部分构造函数设置成private,这样的目的是组织该类的对象实例被创建(在之后的静态方法部分会详细说明)。
父类与子类间的构造函数链
在使用关键字new创建对象时,若类之间存在继承关系,则构造函数的执行顺序会像下面这张图一样:
当创建子类(class3)时,会首先调用其构造函数class3(),但在class3的构造函数本身正式开始之前,首先会调用一次父类(class2)的构造函数,以继承class2中所有的内容。class2的构造函数又会调用上一级父类(class1)的构造函数,构造函数的逐级调用就形成了父类与子类之间的“构造函数链”。
从图上也可以看出,最先执行的是最原始、级别最高的父类的构造函数。由于所有类的源头都是Object类,因此最终会先执行Object类的构造函数。
构造函数链的原则:
有父类才会有子类!
调用毫无痕迹?
也可以让调用变得有痕迹可循。在构造函数的最开头添加上super()方法:
super();
就可以执行父类的构造函数。千万记得在开头!不然会编译报错!
当然,如果懒得写的话,Java也会自动在每个构造函数的开头添加一句super();语句。
父类的构造函数要传参?
如果父类的构造函数需要参数,就直接在super()方法的参数列表中填写即可:
super(parameters);
super()就可以直接看作是父类构造函数在子类中的另一种表示形式。
想要一劳永逸?
如果不想在每个构造函数中都添加super()方法来体现对父类构造函数的调用(尤其是当所需要调用的父类构造函数参数很复杂时),可以仅用一个比较完善的子类构造函数来调用super(),而其他的构造函数只需要在最开头使用this()来调用这个子类构造函数:
this(parameters);
this()中的参数列表和对应的子类构造函数定义的参数列表保持一致。
生存空间、生存期和作用域
生存空间
Java中内存有两大区域——堆和栈。不同的东西会有不同的生存空间,堆是对象的生存空间,而栈则是局部变量和方法调用的生存空间。
还记得之前区分局部变量和对象的实例变量的方法吗?
变量类型 | 声明位置 | 生存空间 |
局部变量 | 声明在类的方法中 | 栈 |
实例变量 | 生命在类中、方法外(类的属性) | 堆 |
在两类生存空间中,行为也是不同的。
栈的行为比较特殊,方法的调用主要在栈中执行,而栈中的所有方法都以堆栈块的形式存在:
被调用的方法会从栈顶进入,随着方法的逐级调用,栈会越来越高;最终结束调用返回也是从栈顶开始,因此后被调用的方法会先返回。
堆中的对象存放则是随机(?可能)的,并没有先后顺序关系。堆区虽然不像栈一样,当方法调用完,堆栈块便被从栈中移除,但堆也存在“垃圾回收机制”,即不再被程序引用的对象会被自动清理。
一个原则:
堆区的内存必须通过new关键字进行分配。
生存期
不管是变量还是对象,都是有一定生存期的,超过了生存期,变量和对象便不复存在。由于Java中对象的生存期直接取决于引用它的引用变量的生存期,因此讨论生存期,主要是讨论变量的生存期。
变量生存期的判断规则——只要声明变量的堆栈块还存在于栈上,该变量即仍存活。(这里的变量可以理解为局部变量)
对于对象内部的实例变量,生存期自然与对象保持一致。因此,只要当前栈上还存在着引用该对象的引用变量,那么该对象及其所包含的实例变量即是存活的。
作用域
局部变量的作用域仅限于声明该变量的代码块内,反映到程序运行过程中,也就是该局部变量所属的堆栈块。当方法调用完毕返回之后,当前有效的作用域也随之改变,在已结束调用的方法内声明的所有局部变量都不再有效。
而对象则难以定义作用域一说。因为对象是通过引用变量发挥作用的,当引用变量所对应的作用域还在工作时,就可以认为对象是在“起作用”的。当然,当所有引用变量都不存在时,对象自然也就失去了作用,不过此时对象也要面临垃圾回收了。
静态
静态对于面向过程的语言来说,可能与全局的意思有些相似。但在面向对象的语言中,“静态”意味着与实例无关!
静态实例变量(以下简称静态变量)
类中定义静态变量的格式如下:
public static type name = xxx;
在权限范围声明public/private和类型名type之间加上关键字static,即定义了一个静态变量。
静态变量的特点:
1.每个类共用一个静态变量;
2.静态变量会在类的任何对象被创建之前、任何静态方法被执行之前就被初始化,可以理解为在程序正式开始执行之前就被初始化;
另外,由于静态变量仍属于实例变量,因此实例变量具有的默认初始值的特点,静态变量同样具有。
每个类共用一个?
类的静态变量对于以该类为模板创建的所有对象都是同步的,也就是说,当任何一个对象通过某种方法改变了静态变量的值时,所有对象的这一变量均会发生改变。而普通的实例变量,则是每个对象自己单独拥有一个,相互之间一般影响。
实例变量 | 静态变量 |
每个实例(对象)拥有一个 | 每个类拥有一个 |
这种设定很大程度上方便了某些工作,比如统计从程序开始运行到现在某个类一共创建过多少个对象。
特殊的静态变量——静态final变量
之前在讲“常量”的时候提到过,Java中最接近于常量的,就是静态final变量。静态final变量的声明格式如下:
public static final type NAME = xxx;
一般静态final变量的名称会采用全部大写的方式,这种方式在其他语言中一般就是常量的表示方式,所以说Java中的静态final变量有时也可以看作就是常量。
让我们再次回顾一下final关键字的用法:
final type 变量 → 变量的值在之后不可以修改;
final type 方法() → 类的方法在子类中不可以被覆写;
final class → 类不可以被继承
正因为不可被修改, 静态final变量必须要赋上初始值,否则声明这样的变量就毫无意义了。编译器也会检查所有的静态final变量是否都进行了初始化。
不过,在初始化的时候有一个小技巧,可以采用声明与初始化相分离的方式来对静态final变量进行定义:
/*声明与初始化同时进行*/
public static final NAME = xxx;
/*声明与初始化分开进行*/
public static final NAME;
static {
NAME = XXX;
}
下面的那种方式,将声明语句和初始化语句放在了不同位置,初始化语句全部用一个以static开头的花括号装起来了
静态方法
与静态变量相似,静态方法的调用也不涉及类的具体实例。静态方法的声明如下:
public static type method() {
}
由于静态方法不涉及具体实例,因此调用静态方法的方式也比较特殊:
class className {
public static type method() {
}
}
className.method()
可以看到,静态方法的调用,是通过类名.方法名来实现的。而普通的方法,则是以对象实例.方法名的方式来实现的。
如果某个类只包含静态方法,就意味着这个类的实例没有必要被创建。因此可以将构造函数设置为private,以防止在程序中创建该类的任何实例。
静态方法使用的注意事项:
静态方法中不能调用非静态变量和非静态方法!
因为静态的本质是在类的任何对象实例被创建之前即可执行,但非静态的变量或方法,则需要有具体的对象实例之后才能够产生和调用。因此如果在静态方法中调用了这两类非静态的东西,则会引发错误。
To be continued……