初始化和清理(1):构造器

    初始化和清理(cleanup)是涉及安全的两个问题。许多C程序的错误都源于程序员忘记初始化变量。特别是正在使用程序库时,如果用户不知道如何初始化库构件(或者是用户必须进行初始化的其他东西),更是如此。清理也是一个特殊问题,当使用完一个元素时,它对你也就不会有什么影响了,所以很容易把它忘记。这样一来,这个元素占用的资源就会一直得不到释放,结果就是资源(尤其是内存)用尽。

    C++引入了构造器(constructor)的概念,这是一个在创建对象时被自动调用的特殊方法。java也采用了构造器,并额外提供了“垃圾回收器”。对于不再使用的内存资源,垃圾回收器能自动将其释放。

一、用构造器确保初始化

    可以假想为编写的每个类都定义一个initialize()方法。该方法的名称提醒你在使用其对象之前,应首先调用initialize()。然而,这同时意味着用户必须记得自己去调用此方法。在java中,通过提供构造器,类的设计者可确保每个对象都会得到初始化。创建对象时,如果其类具有构造器,java就会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行。

    接下来的问题就是如何命名这个方法。有两个问题:第一,所取得名字都可能与类的某个成员名称相冲突;第二,调用构造器是编译器的责任,所以必须让编译器知道应该调用哪个方法。java采用了这种方案:即构造器采用与类名相同的名称。考虑到在初始化期间要自动调用的构造器,这种做法就顺理成章了。

    以下就是一个带有构造器的简单类:

class Rock {
	// This is the constructor
	Rock() {
		System.out.println("Rock ");
	}
}

public class SimpleConstructor {
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Rock();
		}
	}
}

    现在,在创建对象时:new Rock();将会为对象分配存储空间,并调用相应的构造器。这就确保了在你能操作对象之前,它已经被恰当地初始化了。

    请注意,由于构造器的名称必须与类名完全相同,所以“每个方法首字母小写”的编码风格并不适用于构造器。

    不接受任何参数的构造器叫做默认构造器,java文档中通常使用术语无参构造器。和其他方法一样,构造器也能带有形式参数,以便指定如何创建对象。对上述例子稍加修改,即可使构造器接受一个参数:

class Rock {
	// This is the constructor
	Rock(int i) {
		System.out.println("Rock " + i);
	}
}

public class SimpleConstructor {
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Rock(i);
		}
	}
}

    有了构造器形式参数,就可以在初始化对象时提供实际参数。例如,假设类Tree有一个构造器,它接受一个整型变量来表示树的高度,就可以这样创建一个Tree对象:

Tree t = new Tree(12);

    如果Tree(int)是Tree类中唯一的构造器,那么编译器将不会允许你以其他任何方式创建Tree对象。

    构造器有助于减少错误,并使代码更易于阅读。从概念上讲,“初始化”与“创建”是彼此独立的,然而在上面的代码中,你却找不到对initialize()方法的明确调用。在java中,“初始化”和“创建”捆绑在一起,两者不能分离。

扫描二维码关注公众号,回复: 8824682 查看本文章

    构造器是一种特殊类型的方法,因为它没有返回值。这与返回值为空(void)明显不同。对于空返回值,尽管方法本身不会自动返回什么,但仍可选择让它返回别的东西。构造器则不会返回任何东西,你别无选择(new表达式确实返回了对新建对象的引用,但构造器本身并没有任何返回值)。假如构造器具有返回值,并且允许人们自行选择返回类型,那么势必得让编译器知道该如何处理此返回值。

二、方法重载

    任何程序设计语言都具备的一项重要特性就是对名字的运用。当创建一个对象时,也就给此对象分配到的存储空间取一个名字。所谓方法则是给某个动作取的名字。通过使用名字,你可以引用所有的对象和方法。

    将人类语言中存在细微差别的概念“映射”到程序设计语言中时,问题随之而生。在日常生活中,相同的词可以表达多种不同的含义--它们被重载了。特别的含义之间的差别很小时,这种方法十分有用。你可以说“清洗衬衫”、“清洗车”、“清洗狗”。但如果硬要这样说就显得十分愚蠢:“以洗衬衫的方式洗衬衫”、“以洗车的方式洗车”、“以洗狗的方式洗狗”。这是因为听众根本不需要对所执行的动作做出明确的区分。大多数人类语言具有很强的“冗余”性,所以即使漏掉了几个词,仍然可以推断出含义。不需要对每个概念都使用不同的词汇--从具体的语境中就可以推断出含义。

    在java里,构造器是强制重载方法名的一个原因。既然构造器的名字已经由类名所决定,就只能有一个构造器名,那么要想用多种方式创建一个对象该怎么办呢?假设你要创建一个类,既可以用标准方式进行初始化,也可以从文件里读取信息来初始化。这就需要两个构造器:一个默认构造器,另一个取字符串作为形式参数--该字符串表示初始化对象所需的文件名称。由于都是构造器,所以它们必须有相同的名字,即类名。为了让方法名相同而形式参数不同的构造器同时存在,必须用到方法重载。同时,尽管方法重载是构造器所必须的,但它亦可应用于其他方法,且用法同样方便。

    下面这个例子同时示范了重载的构造器和重载的方法:

class Tree {
	int height;

	Tree() {
		System.out.println("Planting a seedling");
		height = 0;
	}

	Tree(int initialHeight) {
		height = initialHeight;
		System.out.println("Creating new Tree that is " + height + "feet tall");
	}

	void info() {
		System.out.println("Tree is " + height + "feet tall");
	}

	void info(String s) {
		System.out.println(s + "Tree is " + height + "feet tall");
	}
}

public class Overloading {
	public static void main(String[] args) {
		for (int i = 0; i < 5; i++) {
			Tree t = new Tree(i);
			t.info();
			t.info("overloaded method");
		}
		new Tree();
	}
}

    创建Tree对象的时候,既可以不含参数,也可以用树的高度当参数。前者表示一颗树苗,后者表示已有一定高度的树木。要支持这种创建方式,得有一个默认构造器和一个采用现有高度作为参数的构造器。

    或许你还想通过多种方式调用info()方法。例如,你想显示额外信息,可以用info(String)方法;没有的话就用info()。要是对明显相同的概念使用了不同的名字,那一定会让人很纳闷。好在有了方法重载,可以为两者使用相同的名字。

三、区分重载的方法

    要是有几个方法有相同的名字,java如何才能知道你指的是哪一个呢?其实规则很简单:每个重载的方法都必须有一个独一无二的参数类型列表。

    稍加思考,就会觉得很合理。毕竟,对于名字相同的方法,除了参数类型的差异以外,还有什么办法能把它们区分开呢?

    甚至参数顺序的不同也足以区分两个方法。不过,一般情况下别那么做,因为这会使代码难以维护:

public class OverloadingOrder {
	static void f(String s, int i) {
		System.out.println("String: " + s + ",int: " + i);
	}

	static void f(int i, String s) {
		System.out.println("int: " + i + "String: " + s);
	}

	public static void main(String[] args) {
		f("String first", 11);
		f(99, "int first");
	}
}

    上例中两个print()方法虽然声明了相同的参数,但顺序不同,因此得以区分。

四、涉及基本类型的重载

    基本类型能从一个“较小”的类型自动提升至一个“较大”的类型,此过程一旦牵涉到重载,可能会造成一些混淆。以下例子中说明了将基本类型传递给重载方法时发生的情况:

public class PrimitiveOverloading {
	void f(long x) {
		System.out.println("调用f(long)");
	}

	void f(double x) {
		System.out.println("调用f(double)");
	}

	public static void main(String[] args) {
		PrimitiveOverloading p = new PrimitiveOverloading();
		byte a = 0;
		char b = 'a';
		p.f(a);
		p.f(b);
	}
}

    如果有方法接受byte或char的参数则会调用该方法,如果其他情况(传入的数据类型小于方法声明的形式参数类型),传入的数据类型会被自动提升。char类型如果找不到接受char参数的方法,就会直接把char提升至int或以上类型。

    如果传入的实际参数大于重载方法声明的形式参数,会出现什么情况呢?例如:

public class Demotion {
	void f(byte i) {
		System.out.println("调用f(byte)方法");
	}

	public static void main(String[] args) {
		Demotion d = new Demotion();
		d.f((byte) 5);
	}
}

    常数5会被当成int类型处理。在这里,方法接受较小的基本类型作为参数。如果传入的实际参数较大,就得通过类型转换来执行窄化转换。如果不这样做,编译器就会报错。

五、以返回值区分重载方法

    有时可能会想:“在区分重载方法的时候,为什么只能以类名和方法的形参列表作为标准呢?能否考虑用方法的返回值来区分呢?”比如下面两个方法,虽然它们同样的名字和形式参数,但却很容易区分它们:

    void f() { }
    int f() {
		return 1;
	}

    只要编译器可以根据语境明确判断出语义,比如在int x=f()中,那么的确可以据此区分重载方法。不过,有时你并不关心方法的返回值,你想要的是方法调用的其他效果(这常被称为“为了副作用而调用”),这时你可能会调用方法而忽略其返回值。所以,如果像下面这样调用方法:

f();

    此时java如何才能判断该调用哪一个f()呢?别人该如何理解这种代码呢?因此,根据方法的返回值来区分重载方法是行不通的。

六、默认构造器

    默认构造器(又名“无参构造器”)是没有形式参数的--它的作用是创建一个“默认对象”。如果你写的类中没有构造器,则编译器会自动帮你创建一个默认构造器。例如:

class Bird {
}

public class DefaultConstructor {
	public static void main(String[] args) {
		Bird b = new Bird();// Default
	}
}

    表达式new Bird()行创建一个新对象,并调用其默认构造器--即使你没有明确定义它。没有它的话,就没有方法可调用,就无法创建对象。但是,如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器:

class Bird2 {
	Bird2(int i) {

	}

	Bird2(double d) {

	}
}

public class NoSynthesis {
	public static void main(String[] args) {
		// Bird2 b=new Bird2(); No default
		Bird2 b = new Bird2(1);
		Bird2 b = new Bird2(1.0);
	}
}

    要是你这样写:new Bird2()。编译器就会报错:没有找到匹配的构造器。这就好比,要是你没有提供任何构造器,编译器就会认为“你需要一个构造器,让我给你制造一个吧”;假如你已经写了一个构造器,编译器则会认为“啊,你已写了一个构造器,所以你知道你在做什么;你是刻意省略了默认构造器。”

发布了71 篇原创文章 · 获赞 2 · 访问量 6152

猜你喜欢

转载自blog.csdn.net/qq_40298351/article/details/104038959