第六章 初始化和清理
两个安全性问题:初始化 和清理 。
C 语言中很多的 bug 都是因为程序员忘记初始化导致的。 忘记清理会造成资源滞留不会被回收,占用内存。 解决方法:
Java 采用了构造器 来初始化 另外还使用了垃圾收集器 (Garbage Collector, GC)去自动回收不再被使用的对象所占的资源。
6.1 利用构造器初始化
Java 会自动调用对象的构造器方法,从而保证初始化。 构造器名称和类名一样。 如果不手写构造器,默认有一个无参构造器。如果自己写了构造器,那么构造器只有你所写的。 构造器没有返回值。
package chapter06 ;
public class Rock {
Rock ( ) {
System . out. println ( "无参构造器" ) ;
}
Rock ( String str) {
System . out. println ( "有参构造器" + str) ;
}
public static void main ( String [ ] args) {
new Rock ( ) ;
new Rock ( "hello world" ) ;
}
}
当创建一个对象时:new Rock() ,内存被分配,构造器被调用。构造器保证了对象在你使用它之前进行了正确的初始化。
6.2 方法重载
一个类中多个方法的方法名相同,但参数列表不同,形成重载。
package chapter06 ;
public class Dog {
public Dog ( ) {
System . out. println ( "狗" ) ;
}
public Dog ( String str) {
System . out. println ( "狗" + str) ;
}
public void eat ( ) {
System . out. println ( "木阿木啊" ) ;
}
public void eat ( String str) {
System . out. println ( "木阿木啊" + str) ;
}
}
6.2.1 区分重载方法
有一条简单的规则:每个被重载的方法必须有独一无二的参数列表 参数列表:参数种类、参数个数、参数顺序,都可区分重载的方法。
package chapter06 ;
public class Dog {
public void eat ( long i) { }
public void eat ( String str) { }
public void eat ( int i) {
}
public void eat ( int i, int j) {
}
public void eat ( int i, String str) { }
public void eat ( String str, int i) { }
}
6.2.2 重载与基本类型
package chapter06 ;
public class Dog1 {
void m ( long i) {
System . out. println ( "long" + i) ;
}
void m ( float i) {
System . out. println ( "float" + i) ;
}
void m ( double i) {
System . out. println ( "double" + i) ;
}
void m ( boolean i) {
System . out. println ( "boolean" + i) ;
}
public static void main ( String [ ] args) {
new Dog1 ( ) . m ( 10 ) ;
new Dog1 ( ) . m ( 10 ) ;
new Dog1 ( ) . m ( 'a' ) ;
new Dog1 ( ) . m ( false ) ;
new Dog1 ( ) . m ( ( byte ) 97 ) ;
new Dog1 ( ) . m ( ( short ) 97 ) ;
}
}
6.2.3 返回值的重载
方法返回值无法区分重载方法。
因为:下面这个看似重载的方法。方法名相同、参数列表也相同,只有返回值不同。但当你只想调用方法中的打印功能时,不在乎返回值,编译器会不知道该调用哪个。
package chapter06 ;
public class Dog {
public int eat ( int i) {
System . out. println ( "yes" ) ;
return 0 ;
}
public String eat ( int i) {
System . out. println ( "no" ) ;
return "" ;
}
}
6.3 无参构造器
如果你创建一个类,类中没有构造器,那么编译器就会自动为你创建一个无参构造器。一旦你显式地定义了构造器(无论有参还是无参),编译器就不会自动为你创建无参构造器。
6.4 this关键字
this 关键字只能在非静态方法内部使用。 如果你在一个类的方法里调用该类的其他方法,不要使用 this,直接调用即可,this 自动地应用于其他方法上了。
package chapter06 ;
public class Apricot {
void pick ( ) { }
void pit ( ) {
this . pick ( ) ;
pick ( ) ;
}
}
this 关键字只用在一些必须显式使用当前对象引用的特殊场合
6.4.1 在构造器中调用构造器
package chapter06 ;
public class Flower {
public Flower ( ) {
this ( "*******" ) ;
}
public Flower ( String str) {
System . out. println ( "flower" + str) ;
}
public Flower ( int i) {
System . out. println ( "flower" + i) :
}
}
6.4.2 static的含义
static 是与类绑定的,而不是对象。 static方法中不会存在 this。 你不能在静态方法中调用非静态方法(反之可以)。静态方法是 为类而创建的,不需要任何对象。
6.5 垃圾回收器
Java 中有垃圾回收器回收无用对象占用的内存。 但垃圾回收器无法回收不是通过new 分配的内存。为此,Java 允许在类中定义一个名为 finalize()的方法。
6.5.1 finalize()的用途
finalize() 工作原理 :一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存。 之所以有 finalize() 方法,是因为在分配内存时可能采用了类似 C 语言中的做法,而非 Java 中的通常做法(本地方法)。因此,不会过多使用 finalize() 方法。
6.5.2 你必须实施清理
6.5.3 终结条件
finalize() 一个有趣的用法,它不依赖于每次都要调用 finalize(),这就是对象终结条件的验证。
package chapter06 ;
public class Book {
boolean checkedOut = false ;
public Book ( boolean checkedOut) {
this . checkedOut = checkedOut;
}
void checkIn ( ) {
checkedOut = false ;
}
@Override
protected void finalize ( ) {
if ( checkedOut) {
System . out. println ( "Error: checked out" ) ;
}
}
}
package chapter06 ;
public class TerminationCondition {
public static void main ( String [ ] args) {
Book book1 = new Book ( true ) ;
book1. checkIn ( ) ;
new Book ( true ) ;
System . gc ( ) ;
}
}
输出:Error: checked out 要是没有 finalize() 方法来验证终结条件,将会很难发现这个 bug。
6.5.4 垃圾回收器如何工作
堆:堆是利用完全二叉树的结构来维护一组数据,然后进行相关操作 栈:顺序栈,链式栈。 垃圾回收机制:
引用计数:
每个对象中含有一个引用计数器,每当有引用指向该对象时,引用计数加 1。当引用离开作用域或被置为 null 时,引用计数减 1。垃圾回收器会遍历含有全部对象的列表,当发现某个对象的引用计数为 0 时,就释放其占用的空间。 缺点:如果对象之间存在循环引用,那么它们的引用计数都不为 0,就会出现应该被回收但无法被回收的情况。对垃圾回收器而言,定位这样的循环引用所需的工作量极大。 停止-复制(stop-and-copy):
顾名思义,这需要先暂停程序的运行,然后将所有存活的对象从当前堆复制到另一个堆,没有复制的就是需要被垃圾回收的。 效率低下主要因为两个原因。其一:得有两个堆,然后在这两个分离的堆之间来回折腾,得维护比实际需要多一倍的空间。其二:在于一旦程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。复制回收器仍然会将所有内存从一处复制到另一处,这很浪费。 标记-清扫(mark-and-sweep):
“标记-清扫” 所依据的思路仍然是从栈和静态存储区出发,遍历所有的引用,找出所有存活的对象。每当找到一个存活对象,就给对象设一个标记,当标记过程完成后,没有标记的对象将被释放,不会发生任何复制动作。标记-清扫” 后剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就需要重新整理剩下的对象。
6.6 成员初始化
成员变量:编译器默认赋予初始值 。
boolean false char byte 0 short 0 int 0 long 0 float 0.0 double 0.0 对象 null 局部变量:必须手动赋予初值
6.6.1 指定初始化
public class MethodInit3 {
int i = f ( ) ;
int f ( ) {
return 11 ;
}
int g ( int n) {
return n * 10 ;
}
}
上述程序的正确性取决于初始化的顺序,而与其编译方式无关。所以,编译器恰当地对 “向前引用” 发出了警告。
6.7 构造器初始化
可以用构造器进行初始化,这种方式给了你更大的灵活性。但是,这无法阻止自动初始化的进行,他会在构造器被调用之前发生。
public class Counter {
int i;
Counter ( ) {
i = 7 ;
}
}
6.7.1 初始化的顺序
6.7.2 静态数据的初始化
无论创建多少个对象,静态数据都只占用一份存储区域。static 关键字不能应用于局部变量,所以只能作用于属性(字段、域)。 初始化的顺序先是静态对象(如果它们之前没有被初始化的话),然后是非静态对 象。
6.7.3 显示的静态初始化
public class Spoon {
static int i;
static {
i = 47 ;
}
}
6.7.4 非静态实例初始化
public class Mugs {
Mug mug1;
Mug mug2;
{
mug1 = new Mug ( 1 ) ;
mug2 = new Mug ( 2 ) ;
System . out. println ( "mug1 & mug2 initialized" ) ;
}
Mugs ( ) {
System . out. println ( "Mugs()" ) ;
}
}
6.8 数组初始化
数组是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。
int [ ] a1 = { 1 , 2 , 3 , 4 , 5 } ;
public class ArraysOfPrimitives {
public static void main ( String [ ] args) {
int [ ] a1 = { 1 , 2 , 3 , 4 , 5 } ;
int [ ] a2;
a2 = a1;
for ( int i = 0 ; i < a2. length; i++ ) {
a2[ i] += 1 ; }
for ( int i = 0 ; i < a1. length; i++ ) {
System . out. println ( "a1[" + i + "] = " + a1[ i] ) ;
}
}
}
输出:
a1[ 0 ] = 2 ;
a1[ 1 ] = 3 ;
a1[ 2 ] = 4 ;
a1[ 3 ] = 5 ;
a1[ 4 ] = 6 ;
6.8.1 动态数组创建
int [ ] a;
Random rand = new Random ( 47 ) ;
a = new int [ rand. nextInt ( 20 ) ] ;
数组的大小是通过 Random.nextInt() 随机确定的
6.8.2 可变参数列表
public class NewVarArgs {
static void printArray ( Object . . . args) {
for ( Object obj: args) {
System . out. print ( obj + " " ) ;
}
System . out. println ( ) ;
}
public static void main ( String [ ] args) {
printArray ( 47 , ( float ) 3.14 , 11.11 ) ;
printArray ( 47 , 3.14F , 11.11 ) ;
printArray ( "one" , "two" , "three" ) ;
printArray ( new A ( ) , new A ( ) , new A ( ) ) ;
printArray ( ( Object [ ] ) new Integer [ ] { 1 , 2 , 3 , 4 } ) ;
printArray ( ) ;
}
}
输出:
47 3.14 11.11
47 3.14 11.11
one two three
A @15db9742 A @6d06d69c A @7852e922
1 2 3 4
有了可变参数,你就再也不用显式地编写数组语法了,当你指定参数时,编译器实际上会为你填充数组。你获取的仍然是一个数组 。
6.9 枚举类型
public enum Spiciness {
NOT, MILD, MEDIUM, HOT, FLAMING
}
public class SimpleEnumUse {
public static void main ( String [ ] args) {
Spiciness howHot = Spiciness . MEDIUM;
System . out. println ( howHot) ;
}
}
输出:
MEDIUM
可以将 enum 当作其他任何类。事实上,enum 确实是类,并且具有自己的方法。 由于 switch 是在有限的可能值集合中选择,因此它与 enum 是绝佳的组合。
6.10 本章小结
构造器能保证进行正确的初始化和清理(没有正确的构造器调用,编译器就不允许创建对象) 垃圾回收器会自动地释放所有对象的内存,极大地简化了编程,并加强了内存管理上的安全性。然而,垃圾回收器确实增加了运行时开销,速度问题仍然是它涉足某些特定编程领域的障碍。 由于要保证所有对象被创建,实际上构造器比这里讨论得更加复杂。特别是当通过组合或继承创建新类的时候,这种保证仍然成立,并且需要一些额外的语法来支持。