随着计算机革命的发展,“不安全”的编程方式已经逐渐成为编程代价高昂的主要原因之一。

初始化和清理,正是涉及安全的两个问题。

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

一、用构造器确保初始化

在Java中,通过提供构造器,类的设计者可以确保每个对象都会得到初始化。创建对象时,如果其类具有构造器,Java 就会在用户可以操作对象之前自动调用相应的构造器来确保初始化的进行。

构造器的命名问题,C++ 中构造器的名字与类的名字也是相同的,Java 中也采用了这种方案。

创建一个类,之后再创建对象:

1
new Rock();

将会为对象分配存储空间,并调用相应的构造器,来确保操作对象之前,它已经被恰当地初始化了。

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

不接受任何参数的构造器叫做默认构造器,Java 中术语称为无参构造器,构造器也可以像其他方法一样带有形式参数,以便指定如何创建对象。

1
2
3
4
5
6
7
8
9
10
11
class Person {

Person() {
System.out.println("默认的构造器");
}

Person(int age) {
System.out.println("有参数的构造器");
System.out.println("这个人" + age + "岁了");
}
}

创建对象时,就可以提供给对象实际参数了:

1
new Person(8);

注意

  • 如果类中没有构造器,那么创建对象时就只能使用无参构造器创建,编译器会自动帮你创建一个默认构造器。
  • 如果类中已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器了,也就不能以其他的任何方式创建对象了。

二、方法重载

当创建一个对象时,也就给此对象分配到的存储空间取了一个名字,方法则是给某个动作取得名字。

大多数程序语言(例如C语言)要求每个方法(函数)都提供一个独一无二的标识符,所以绝对不能用一个函数做了一件事之后,又有一个同名函数做另一件事。每个函数都要有唯一的名称。

在 Java 和 C++ 里,构造器是强制重载方法名的另一个原因。如果你想通过无参构造器创建一个对象,也想用有参构造器创建此对象,这时候类里面就需要两个构造器,由于都是构造器,所以名字必须相同,此时就必须使用到方法重载。同样,方法重载也可以用于其他的方法。例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {

int age;

Person() {
System.out.println("默认的构造器");
}

Person(int age) {
System.out.println("有参数的构造器");
System.out.println("这个人" + age + "岁了");
}

info() {
System.out.println("这个人" + age + "岁了");
}

info(String name) {
System.out.println("叫" + name + "的这个人" + age + "岁了");
}
}

1、区分重载方法

有几个方法有相同的名字,如何区分你使用的是哪一个呢?规则如下:每个重载方法都必须有一个独一无二的参数类型列表,甚至参数顺序的不同也足以区分两个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Demo {

/*
* 方法两个参数的类型是相同的,不能按照顺序区分,编译器报错:Duplicate method process(String, String) in type Demo
*
* public void process(String id, String name) {
*
* }
*
* public void process(String name, String id) {
*
* }
*/

public void hhh(int age, String name) {

}

public void hhh(String name, int age) {

}

public void resolve(String id, String name, int age) {
System.out.println(id+" "+name+" "+age);
}

public void resolve(int age, String name, String id) {
System.out.println(id+" "+name);
}
}

2、涉及基本类型的重载

基本数据类型能从一个“较小”的类型自动提升至一个“较大”的类型,此过程涉及到重载,可能会造成些许混淆。

如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际的数据类型就会得到提升。char 略有不同,如果无法找到恰好接受 char 参数类型的方法,就会把 char 直接提升至 int 型。

如果传入的实际参数类型大于重载方法声明的形式参数,编译器就会要求你必须通过类型转换来进行窄化转换,否则编译器就会报错。

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

重载方法的时候,只能以类名和方法的形参列表作为标准,不可以用方法的返回值来区分。

三、this 关键字

如果希望在某个方法内部调用对当前对象的引用,this 关键字就有用处了。this 关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。

1、在构造器中调用构造器

在一个类中写了多个构造器,有时候可能会想在一个构造器中调用另一个构造器,以避免重复的代码,可以用 this 关键字做到这一点。

通常写 this 时,都是指“这个对象”或“当前对象”,而且它本身表示对当前对象的引用。如果为 this 添加了参数列表,那么就有不同的含义了。这将产生对符合此参数列表的某个构造器的明确调用,这样调用其它构造器就有了直接的途径了。

但是要注意的几点情况如下:

  • 尽管可以在一个构造器中用this调用另一个构造器,但是不能调用两个。
  • 调用另一个构造器时,要将其置于该构造器的最起始处,否则编译出错。
  • 在构造器之外,编译器禁止在其他任何方法中调用构造器。

四、清理:终结处理和垃圾回收

Java有垃圾回收器负责回收无用对象占据的内存资源,但是当你的对象(并非使用 new 创建)获得了一块“特殊”的内存区域,由于垃圾回收期只知道释放那些经由 new 分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存。为了应对这种情况 Java 允许在类中定义一个名为 finalize() 的方法。工作原理如下:一旦垃圾回收器准备释放对象占用的存储空间,将首先调用其 finalize() 方法,并在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。

注意finalize() 并不是 C++ 中的析构函数(在 C++ 中销毁对象必须使用这个函数)。C++ 中对象一定会被销毁,然而在 Java 中对象却并非总是被垃圾回收。

注意java.lang.Object.finalize() 方法在 Java 9 版本中被标记为弃用,并且在后续的版本中可能会被删除。

1
2
3
4
5
public class Object {

@Deprecated(since="9", forRemoval=true)
protected void finalize() throws Throwable { }
}

官方给出的描述中,说使用 finalization 可能会导致安全性、性能和可靠性方面的问题。使用 java.lang.ref.Cleanerjava.lang.ref.PhantomReference 是在对象变得不可访问时释放资源的更安全的方法。或者添加一个关闭方法以显式释放资源,并实现 AutoCloseable 以启用 try-with-resources 语句。

JEP 421: Deprecate Finalization for Removal - OpenJDK

1、finalize() 的用途

finalize() 不应该作为通用的清理方法。垃圾回收只与内存有关,也就是说使用垃圾回收器的唯一原因是为了回收程序不再使用的内存。无论对象是如何创建的,垃圾回收器都会负责释放对象占据的所有内存。

finalize() 的需求被限制在一种特殊情况:通过某种创建对象方式以外的方式为对象分配了存储空间。主要发生在使用“本地方法”的情况下,本地方法是一种在 Java 中调用非 Java 代码的方式,本地方法目前只支持 C 和 C++,C 和 C++ 又可以调用其它语言的代码。所以 finalize() 并不是进行普通清理工作的合适场所。

2、必须实施清理

要清理一个对象,用户必须在需要清理的时刻调用执行清理动作的方法。

C++ 中,所有对象都应该被销毁。如果在 C++ 中创建了一个局部对象(在栈上创建,Java 中不可以)此时的销毁动作发生在以“右花括号”为边界的、此对象作用域的末尾处。如果对象是使用 new 创建的,那么当程序要使用 delete 操作符时,就会调用相应的析构函数。如果忘记调用 delete,那么就永远不会调用析构函数,这样就会出现内存泄漏。

3、终结条件

finalize() 可以用于验证对象的终结条件。

当某个对象可以被清理了,这个对象应该处于某种状态,使它占用的内存可以被安全地释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Book {  

boolean checkOut = false;

public Book(boolean checkOut) {
this.checkOut = checkOut;
}

void checkIn() {
checkOut = false;
}

@Override
protected void finalize() {
if (checkOut) {
System.out.println("error: checked out");
super.finalize();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class TerminationCondition {  

public static void main(String[] args) {
Book novel = new Book(true);
// proper cleanup
novel.checkIn();
// Drop the reference, forget to clean up
new Book(true);
// force garbage collection & finalization
System.gc();
}

}

上例中,所有的 Book 都应该被 checkIn(),但是由于错误,有一本书没有签入。要是没有 finalize() 来验证终结条件,将很难发现这种缺陷。System.gc() 用来强制执行终结动作。

4、垃圾回收器如何工作

Java 中的对象在堆上分配代价并不是非常高昂,垃圾回收器对于提高对象的创建速度有着明显的效果。

五、成员初始化

Java 尽力保证:所有变量在使用前都能得到恰当地初始化。

  • 方法的局部变量是必须要进行初始化的。
  • 类的每个数据成员(字段)如果没有初始化会有一个初始值。

可以调用某个方法来提供初始化值,但是在此过程中避免发生向前引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Demo{

int i = f();

int j = g(i);

int f() {
return 2;
}

int g(int i) {
return i*10;
}
}

public class Demo2 {

// error
int j = g(i);

int i = f();

int f() {
return 2;
}

int g(int i) {
return i*10;
}
}

六、构造器初始化

用构造器进行初始化,可以在运行时调用方法或执行某些动作来确定初值,增大编程灵活性。

1、初始化顺序

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

无论创建多少个对象,静态数据都只占用一份存储区域。static关键字不能用于局部变量,因此它只能作用于域。

例子如下:

1
2
3
4
5
6
7
8
9
10
class Bowl {  

public Bowl(int marker) {
System.out.println("Bowl(" + marker + ")");
}

void f1(int marker) {
System.out.println("f1(" + marker + ")");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Table {  

static Bowl bowl = new Bowl(1);

public Table() {
System.out.println("Table()");
bowl2.f1(1);
}

void f2(int marker) {
System.out.println("f2(" + marker + ")");
}

static Bowl bowl2 = new Bowl(2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Cupboard {  

Bowl bowl3 = new Bowl(3);

static Bowl bowl4 = new Bowl(4);

public Cupboard() {
System.out.println("Cupboard()");
bowl4.f1(2);
}

void f3(int marker) {
System.out.println("f3(" + marker + ")");
}

static Bowl bowl5 = new Bowl(5);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class StaticInitialization {  

public static void main(String[] args) {
System.out.println("creating new Cupboard() in main");
new Cupboard();
System.out.println("creating new Cupboard() in main");
new Cupboard();
table.f2(1);
cupboard.f3(1);
}

static Table table = new Table();
static Cupboard cupboard = new Cupboard();
}

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)

由上例可知,静态初始化只有在必要的时刻才会进行。如果不创建 Table 对象,也不引用其中的内容,那么静态的 Bowl 就永远不会被创建。只要在第一个 Table 对象被创建(或第一次访问静态数据)时,他们才会被初始化。此后,静态对象不会再次被初始化。

初始化顺序是先静态对象,而后是非静态对象。

2、显式地静态初始化

Java允许将多个静态初始化动作组织成一个特殊的“静态子句”(静态块)。

1
2
3
4
5
6
7
8
public class Demo {

static int i;

static {
i = 55;
}
}

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

3、数组初始化

数组只是相同类型的、用一个标识符名称封装在一起的一个对象序列或基本类型数据序列。

1
int[] array;

为了给数组创建相应的存储空间,必须写初始化表达式。

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

所有数组都有一个固有的成员,可以通过它获得数组中包含了多少个元素,但是它不能被修改,这个成员就是 length。数组起始位置为0,所以最大下标为 length-1,如果超过这个边界,C 和 C++ 会默认接受,并允许你访问所有的内存,Java 一旦下标过界,就会出现运行时错误(异常)。

相关链接

JEP 421: Deprecate Finalization for Removal - OpenJDK

OB tags

#Java