Java 中的对象初始化(构造器)以及this、super 和 static 的作用
0 Java 是面向对象的语言
在 Java 语言的世界中,万物皆对象。我们通过对象的名字——标识符——来操作对象,它们实际上是对象的引用。在创建对象引用的时候,需要将其指向一个特定的对象,即对象在堆内存中的存储地址,否则这个标识符是不能使用的。这类似于C++中的空指针,但标识符引用为空(null)和没有引用是不同的,当标识符所指定的数据类型中包含了静态成员,那么即使该标识符引用为空也是可以直接使用静态成员的,静态成员可以不依赖于对象而直接从类访问。举个栗子🌰:
public class Test {
public static void main(String[] args) {
Human person1 = null;
System.out.println(person1); // null
System.out.println(person1.habit); // 空引用可以直接访问静态成员
System.out.println(Human.habit); // 不依赖于对象而直接从类访问
Human person2; // 对象不能使用,虽然不会编译报错
// System.out.println(person2.habit); // error: 可能尚未初始化变量person2
}
}
class Human {
public static String habit = "The Earth.";
String name;
}
1. 对象初始化
Java 中使用构造器保证对象在创建过程中的初始化工作。当通过 new 创建对象时,内存被分配,构造器被调用。构造器是一种特殊的方法,与 C++ 中一样,Java 中的构造器名称与类名相同,它没有返回值,连空值 void 都没有。为了通过不同的方式创建对象,就要用到方法重载定义不同的构造器。
重载的方法之间唯一的区分方式就是参数列表(参数类型、个数和顺序),通过返回值是不能重载方法的,原因是我们很多时候并不关心返回值,还有就是构造器这种根本没有返回值的方法的存在。
构造器可以不用显式地给出,这时编译器会创建一个默认的无参构造器。而一旦我们在类中创建了任何显式的构造器,无参构造器就不会被自动创建。下面是一个例子。
class Bowl {
float height;
String shape;
Bowl(String s) { shape = s; }
Bowl(float h) { height = h; }
}
public class BowlTest {
public static void main(String[] args) {
// Bowl b0 = new Bowl(); // error: 对于Bowl(没有参数), 找不到合适的构造器
Bowl b1 = new Bowl("round");
Bowl b2 = new Bowl(1.2f);
}
}
建议的做法是,手动编写出类的无参构造器,这样做的好处:一是不用担心在重载出其他有参构造器过后,无参构造器自动消失了;二是可以在显示声明的无参构造器中添加我们想要的初始化操作,比如调用其它的有参构造器设置属性默认值。
2. this 关键字
它用于在对象的方法内部,获得对当前对象的引用。使用 this 关键字的一些场景有:
- 在构造器中调用其它构造器;
- 消除参数列表中的变量和成员变量的同名冲突;
- 成员方法中返回对象本身;
- 向成员方法传递对象本身。
如下例子包含了上述几种情况:
class UseCup {
static Cup drink(Cup cup, float drinkHeight) {
cup.waterLevel -= drinkHeight;
return cup;
}
}
class Cup {
String shape;
float height;
float waterLevel = 0;
Cup(String s) { shape = s; }
Cup(float h) { height = h; }
Cup(String shape, float height) {
this(shape); // 1. 在构造器其中调用构造器,必须写在第一行
// this.Cup(height); // 并且只能通过 this 调用一次构造器
this.height = height; // 2. 使用 this 避免混淆冲突
}
Cup() { this("round", 12.5f); }
Cup addWater(float addHeight) {
waterLevel += addHeight;
return this; // 3. 方法中返回当前对象的引用
}
Cup getDrunk(float drinkHeight) {
return UseCup.drink(this, drinkHeight); // 4. 向其他方法传递当前对象
}
}
public class CupTest {
public static void main(String[] args) {
Cup c = new Cup();
c.addWater(3.2f).addWater(0.5f); // 可以连续执行行多次操作
System.out.println("杯中水高:" + c.waterLevel);
c.getDrunk(1.2f);
System.out.println("杯中水高:" + c.waterLevel);
}
}
就像 Cup(String shape, float height) 中展示的那样,我们只能通过this
调用一次构造器,this(shape) 会直接调用与参数列表相匹配构造器 Cup(String s)。而且调用构造器的语句必须写在第一行。这样做是很合理的,因为我们理应在对象被创建出来后再做可能需要的一些其它的操作,而不是在对象创建之前就去做。addWater() 中 this 返回了当前对象的引用,因此可以在对象上连续执行多次该操作。这里向其他方法传递当前对象的原因是我们不想再重复地编写工具类 UseCup 中的代码,尤其是在工具类中的方法行为在多个类中都需要的情况下,这时通过调用该外部工具类方法避免重复代码的出现,那么就要使用 this 传递自身引用。
3. super 关键字
在继承关系中,子类可以通过 super 关键子来访问父类中的资源(构造器、成员变量和成员方法)。
-
子类构造器通过
super(参数);
调用父类的构造器创建父类对象。从前面可以知道,当我们不在类中手动编写任何构造器时,编译器会默认为其添加无参构造器。同样,当我们不在子类的构造器中使用
super
关键字调用任何父类构造器时,编译器会默认添加super();
以调用父类的无参构造器。因此,这种机制保证了在子类对象被创建之前,其父类对象必定已经存在。当然,我们可以通过 super 调用父类中已有的任意构造器,创建出我们想要的默认父类对象。
注意:使用 super(参数);
调用父类构造器的语句必须在子类构造器的第一行。这样规定使得this
和super
是不能一起出现的,这是为了保证逻辑合理性而做的规定。因为一旦在当前构造器中使用this
调用自身另外的构造器,程序会在另外的那个构造器中通过super
调用父类构造器(如果没有显示添加则会调用父类的无参构造器),所以在当前构造器中就不能再次使用super
以免二次创建父类对象。反过来说,一旦在当前构造器中使用super
调用父类构造器,如果再使用this
调用自身另外的构造器,同样会出现二次创建父类对象的情况。下面例子展示了正确的显式使用super
和this
的情况:
class Pen {
double length;
Pen() {}
Pen(double length) {
this.length = length;
}
}
class Pencil extends Pen{
Pencil() {
this(15.5); // 调用自身的有参构造器
}
Pencil(double length) {
super(length); // 调用父类的有参构造器
}
}
-
通过
super
关键字访问父类的非私有属性/方法。由于子类自动拥有父类的一切非私有成员,所以子类访问父类的非私有属性其实完全不需要
super
。同样,在子类没有重写父类方法的情况下,是没必要通过super
关键字调用父类方法的。只有在父类方法被子类重写的情况下,如果还需要使用父类该方法,则须通过super.父类方法名()
的方式调用。
4. static 关键字
this
和super
关键字都是与对象绑定的关键字,因此它们都只能用在与对象相关的成员方法中。而使用static
修饰的属性或方法不属于任何对象,它们是静态资源,它们直接属于类本身。在 Java 的对象世界中,静态资源表示的是所属类的对象们的具有的共性部分,它不属于任何特定的对象,或者它说属于该类的所有对象。比如所有人居住的星球都是地球(至少目前是),那么planet
就可以作为Person
类的静态属性,其值为地球
。
一个类中的静态资源的加载总是先于非静态资源的,所以前者不能访问后者,即静态方法不能访问成员属性,不能调用成员方法;而反过来是可以的,即成员方法可以自由访问静态变量或调用静态方法。当然,类中静态资源之间是可以相互访问的,就像非静态资源之间可以相互访问一样,即静态方法可以访问静态属性或调用其他的静态方法。