《 Thinking in Java 》第五章 初始化与清理

用构造器确保初始化

在 Java 中,通过提供构造器,类的设计者可确保每个对象都会得到初始化。创建对象时,如果类具有构造器,Java 就会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行。
命名的方案:构造器采用与类相同的名称。


从概念上讲,“初始化” 与 “创建” 是彼此独立的,然而在 Java 的代码中,找不到对 initialize() 的方法的明确调用。在 Java 中,“初始化” 和 “创建” 捆绑在一起,两者不能分离。

方法重载

在 Java 里,构造器是强制重载方法名的一个原因。由于构造器的名字是确定的,但是想要的不同的方式来创建对象,就需要用到方法重载

class Tree {
	int height;
	Tree() {
		height = 0;
	}
	Tree(int initialHeight) {
	height = initialHeight;
	}
}

区分重载方法

规则:每个重载的方法都必须有一个独一无二的参数类型列表。甚至参数顺序的不同也足以区分两个方法,但是不建议这样做。

涉及基本类型的重载

基本类型能从一个“较小” 的类型自动提升至一个 “较大” 的类型,因此要注意。
规则大概如此:有某个重载方法接受相应的类型,就会被调用。至于,如果传入的数据类型小于声明的形式参数类型,实际数据类型就会被提升。char 型略有不同,会把 char 直接提升至 int 型。
而如果传入的实际参数较大,就得通过类型转换来窄化转换。

以返回值区分重载方法

为什么在区分类的重载方法的时候,我们只能以入参作为标准,而不能用返回值作为标准。
请参看下面两个方法:

	void fun();
	int fun();

如果编译器可以根据语境来判断执行哪一个方法的话,那么当 执行 int i = fun(); 的时候,我们很容易判断出来,应该调用第二个重载方法。
但是直接执行 fun() ,这个时候,我们就不知道调用哪个方法了,所以根据返回值来区分重载方法是行不通的。

总结:重载和返回值修饰符均无关。

默认构造器

默认构造器没有参数,如果没写,会自动创建。但是,在定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器。

this 关键字

如果有同意类型的两个对象 a , b。如何才能让这两个对象都调用某一 peel() 方法?

class Banana {
	void peel(int i) {
	}
}

public class BananaPeel {
	public static void main(String[] args) {
		Banana a = new Banana(),
		       b = new Banana();
		a.peel(1);
		b.peel(2);
	}
 }

为了能用简便、面向对象的语法来编写代码——即“发送消息给对象”,编译器做了一些幕后工作。它暗自把“所操作对象的引用”作为第一个参数传递给 peel() 。所以如下:

	Banana.peel(a, 1);
	Banana.peel(b, 2);

这是内部表示形式。我们并不能这样书写代码。
如果希望在方法内部获得对当前对象的引用,可以使用 this 关键字。this 关键字只能在方法内部使用,表示对“调用方法的那个对象” 的引用。在方法内部调用同一个类的另一方法,就不必使用 this,直接调用即可。编译器能自动添加。
方便用于返回当前对象:

public class Leaf {
	int i = 0;
	Leaf increment() {
		i++;
		return this;
	}
}

在构造器中调用构造器

一个类中的多个构造器,在其中一个调用另一个,可用 this 关键字做到。
在构造器中,如果为 this 添加了参数列表,这将产生对符合此参数列表的某个构造器的明确调用。

class A {
	int i = 0;
	A(int i) {
		i = 1;
	}
	A(String str, int i) {
		this(i);
		System.out.print("str");
	}
}

需要注意的地方

  • this 调用必须置于起始处。
  • 不能调用两个

另外,this 也可以用来解决参数名称和数据名称重名的情况,不再赘述。

static 的含义

static 方法就是没有 this 的方法。
在 static 方法内部不能调用非静态方法。反过来可以。在没有创建任何对象的前提下,仅可以通过类本身来调用 static 方法,它很像全局方法。在类中置入 static 方法就可以访问其他 static 方法和 static 域。
使用 static 方法,由于不存在 this ,所以不是通过“向对象发送消息” 的方式来完成的。

成员初始化

  • 对于方法的局部变量,Java 以编译时错误的形式来管理。
void f() {
	int i;
	i ++ ;//Error -- i not initalized
}

这样会得到一条出错消息。

  • 对于类的数据成员

如果是基本类型,类的每个基本类型数据成员都会有一个初始值,具体参照“一切都是对象”章节。

class A {
	int i ;
	public static void main(String[] args) {
		System.out.print(i);
	}
 }
 //Output: 1

如果是对象引用,不初始化,此引用会获得一个特殊值 null。不再做演示。

指定初始化

  1. 直接在定义类成员变量的地方赋值。
  2. 调用某个方法来提供初值。

但需要注意一处:

class A {
	//int j = g(i);   (1)
	int i = f();     //(2)
	int f() { return 11; }
	int g(int n) { return n * 10; }
}

上述程序的正确性取决于初始化的顺序,而与编译方式无关。所以,编译器恰到地对“向前引用”发出了警告。
如果修改 (1) 到 (2) 之后,则编译正常。

构造器初始化

在运行时刻,可以调用方法或执行某些动作来确定初值,但要牢记无法组织自动初始化的进行,它将在构造器调用之前发生。
总的来说,对于所有基本类型和对象引用,包括在定义时已经指定初值的变量,都会先被自动初始化,然后才被赋予你规定的值。

初始化顺序

在类的内部,变量定义的先后顺序决定了初始化的顺序。即使,散布于方法定义之间,仍旧会在任何方法被调用之前得到初始化,包括构造器。

静态数据的初始化

无论创建多少个对象,静态数据都只占用一份存储区域。static 关键字不能应用于局部变量,只能作用于域。
当然,如果没有进行初始化,它就会获得基本类型的标准初值,引用默认初值 null 。

总结一下对象的创建过程

  1. 即使没有显示的使用 static 关键字,构造器实际上也是静态方法。因此,当首次创建类型为 Dog 的对象时,或者 Dog 类的静态方法/静态域首次被访问时,Java 解释器必须查找类路径,以定位 Dog.class 文件。
  2. 然后载入 Dog.class (这样会创建一个 Class 对象),有关静态初始化的所有动作都会执行。因此,静态初始化只在 Class 对象首次加载的时候进行一次。
  3. 当用 new Dog() 创建对象的时候,首先将在堆上为 Dog 对象分配足够的存储空间。
  4. 这块存储空间会被清零,这就自动地将 Dog 对象中的所有基本类型拘束都设置成了默认值,而引用被设置成了 null。
  5. 执行所有出现于字段定义出的初始化动作。
  6. 执行构造器。这可能会牵涉到很多动作,尤其是涉及继承的时候。

显示的静态初始化

Java 允许将多个静态初始化动作组织成一个特殊的“静态子句” ,有时也叫作“静态块”。

public class Spoon {
	static int i;
	static {
		i= 47;
	}
}

与其他静态初始化动作一样,这段代码仅执行一次:

  • 当首次生成这个类的一个对象时
  • 首次访问属于那个类的静态数据成员时( 即便从未生成那个类的对象)。
class Cup {
	Cup(int i ) {
	}
	f() {
	}
}

class Cups {
	static Cup cup1;
	static Cup cup2;
	static {
		cup1 = new Cup(1);
		cup2 = new Cup(2);
	}
}

public class ExplicitStatic {
	public static void main(String[] args ) {
		Cups.cup1.f(); // (1)
	}
	static Cups cups1 = new Cups(); //(2)
}

无论是用 (1) 亦或是 (2) Cups 的静态初始化动作都会得到执行。如果同时注释掉就不会进行。

非静态实例初始化

有被成为实例初始化的类似语法,用来初始化每一个对象的非静态变量。

class Mug {
}
public class Mugs {
	Mug mug1;
	Mug mug2;
	{
		mug1 = new Mug(1);
		mug2 = new Mug(2);
	}
}

比静态初始化子句少了 static 关键字。这种语法对于支持“匿名内部类”的初始化是必须的,但是它也保证无论调用了哪个显示构造器,某些操作都会发生。

数据初始化

数组是相同类型、用一个标识符名称封装到一个对象序列或基本类型数据序列。
为了给数组创建相应的存储空间,必须写初始化表达式。对于数组,初始化动作可以出现在代码的任何地方,但也可以使用一种特殊的初始化表达式,必须在创建数组的地方出现。这种特殊的初始化是由一对花括号括起来的值组成的。在这种情况下,存储空间的分配将有编译器负责。

int[] a1 = {1, 2, 3, 4, 5 };

在 Java 中在没有数组的时候定义一个数组引用,

int[] a2;

在需要的时候,可以将一个数组赋值给另一个数组,

a2 = a1;

其实真正做的只是复制了一个引用。因此修改 a2 的内容也会影响到 a1
可以用 new 创建数组,并且没有赋值的基本数据类型值会自动初始化程空值( 对于数字和字符,就是 0,对于布尔型,是 false )。
Arrays.toStrring() 方法可以产生一维数组的可打印版本。


除了用 for 循环初始化之外,也可以用花括号括起来的列表来初始化对象数组。有两种形式

public class ArrayInit {
	public static void main(String[] args) {
		Integer[] a = {
			new Integer(1),
			new Integer(2),
			3,
			};
		Integer[] b = new Integer[] {
			new Integer(1),
			new Integer(2),
			3,
			};
	}
}

最后一个逗号都是可选的。
总结:三种方法,for 循环的使用受限制。

可变参数列表

在没有可变参数列表之前,形式参数只能传递一个数组的引用,具体表现在,需要先 new 一个数组对象。而,在有了可变参数之后的,就像下面这样

class  A {
	static void f(Object...objects) {

	}
	public static void main(String[] args) {
		f();
		f(new Integer(47), new Float(3.14));
		f(47, 3.14);
		f("one", "two", "three");
		f((Object) new Integer[]{1, 2, 3, 4});	
	}
}

参上,不用显示地编写数组语法,指定参数时,编译器实际上会为你填充数组。获取的仍旧是一个数组。
而且不仅仅只是从元素列表到数组的自动转换,最后一行,传递了一个数组,编译器会发现它已经是一个数组,所以不会在其上执行任何转换。而第一行则说明,传递 0 个参数也是可行的,当具有可选的尾随参数时,这一特性就会很有用。

void f(int required, String...striings) {
} 

在重载了多个可变参数时,再调用 f() 时会产生错误,欲解决此问题,就要增加一个非可变参数来解决,如上。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值