Java编程思想读书笔记——初始化与清理(一)

时间:2020-12-29 12:09:36

第五章 初始化与清理

5.1 用构造器确保初始化
Java中也有类似C++中的构造器(constructor)的概念,其名称与类名相同,没有返回值。

在创建对象时,将会为对象分配存储空间,并调用相应的构造器。

构造器按参数的有无划分为无参构造器(默认构造器)和有参构造器。

5.2 方法重载
构造器重载:默认构造器和有参构造器;
普通方法重载:方法名相同,与返回值和访问修饰符无关,且必须有独一无二的参数类型列表。

参数顺序的不同也可以造成方法重载,但是建议避免这种情况,因为这会导致代码难以维护。

涉及基本类型的重载

public class PrimitiveOverloading {

void f1(char x){
System.out.print("f1(char) ");
}

void f1(byte x){
System.out.print("f1(byte) ");
}

void f1(short x){
System.out.print("f1(short) ");
}

void f1(int x){
System.out.print("f1(int) ");
}

void f1(long x){
System.out.print("f1(long) ");
}

void f1(float x){
System.out.print("f1(float) ");
}

void f1(double x){
System.out.print("f1(double) ");
}

void f2(byte x){
System.out.print("f2(byte) ");
}

void f2(short x){
System.out.print("f2(short) ");
}

void f2(int x){
System.out.print("f2(int) ");
}

void f2(long x){
System.out.print("f2(long) ");
}

void f2(float x){
System.out.print("f2(float) ");
}

void f2(double x){
System.out.print("f2(double) ");
}

void f3(short x){
System.out.print("f3(short) ");
}

void f3(int x){
System.out.print("f3(int) ");
}

void f3(long x){
System.out.print("f3(long) ");
}

void f3(float x){
System.out.print("f3(float) ");
}

void f3(double x){
System.out.print("f3(double) ");
}

void f4(int x){
System.out.print("f4(int) ");
}

void f4(long x){
System.out.print("f4(long) ");
}

void f4(float x){
System.out.print("f4(float) ");
}

void f4(double x){
System.out.print("f4(double) ");
}

void f5(long x){
System.out.print("f5(long) ");
}

void f5(float x){
System.out.print("f5(float) ");
}

void f5(double x){
System.out.print("f5(double) ");
}

void f6(float x){
System.out.print("f6(float) ");
}

void f6(double x){
System.out.print("f6(double) ");
}

void f7(double x){
System.out.print("f7(double) ");
}

void testConstVal(){
System.out.print("5: ");
f1(5);
f2(5);
f3(5);
f4(5);
f5(5);
f6(5);
f7(5);
System.out.println();
}

void testChar(){
char x = 'x';
System.out.print("char: ");
f1(x);
f2(x);
f3(x);
f4(x);
f5(x);
f6(x);
f7(x);
System.out.println();
}

void testByte(){
byte x = 0;
System.out.print("byte: ");
f1(x);
f2(x);
f3(x);
f4(x);
f5(x);
f6(x);
f7(x);
System.out.println();
}

void testShort(){
short x = 0;
System.out.print("short: ");
f1(x);
f2(x);
f3(x);
f4(x);
f5(x);
f6(x);
f7(x);
System.out.println();
}

void testInt(){
int x = 0;
System.out.print("int: ");
f1(x);
f2(x);
f3(x);
f4(x);
f5(x);
f6(x);
f7(x);
System.out.println();
}

void testLong(){
long x = 0;
System.out.print("long: ");
f1(x);
f2(x);
f3(x);
f4(x);
f5(x);
f6(x);
f7(x);
System.out.println();
}

void testFloat(){
float x = 0;
System.out.print("float: ");
f1(x);
f2(x);
f3(x);
f4(x);
f5(x);
f6(x);
f7(x);
System.out.println();
}

void testDouble(){
double x = 0;
System.out.print("double: ");
f1(x);
f2(x);
f3(x);
f4(x);
f5(x);
f6(x);
f7(x);
System.out.println();
}

public static void main(String[] args){
PrimitiveOverloading p = new PrimitiveOverloading();
p.testConstVal();
p.testChar();
p.testByte();
p.testShort();
p.testInt();
p.testLong();
p.testFloat();
p.testDouble();
}
}

1) 整数常数值将作为int类型;
2) char类型的变量如果小于方法中形参的类型,其会直接被提升为int类型;
3) byte/short/int/long/float类型的变量如果小于方法中形参的类型,其会直接提升为与形参相同的类型。
4) 要想传入比形参类型大的变量,就必须对变量进行窄化转换。

5.3 默认构造器
默认构造器是非显式的,即当类中无其他构造器时,编译器会自动创建一个默认构造器来创建对象。
但是如果类中声明了其他构造器,编译器则不会自动创建默认构造器,必须显式声明默认构造器,才可以使用默认构造器创建对象。

5.4 this关键字
1)this关键字只能在方法内部使用,表示调用方法的那个对象的引用;
2)只有在需要明确指出当前对象的引用的情况下,才需要使用this关键字;
3)this关键字可用于在一个构造器中调用另一个构造器,以避免重复代码;
4)static方法就是没有this的方法,在static方法内部不能调用非静态方法。

5.5 清理:终结处理和垃圾回收
finalize方法的执行时机:只有当JVM面临内存耗尽的情况,它才会执行垃圾回收,调用finalize方法。

1) finalize方法不一定能及时执行;
2) finalize方法不一定会被执行,因为垃圾回收不一定会发生,而在当程序结束时,都没有发生垃圾回收的情形,程序所使用的资源也会交还给操作系统;
3) finalize方法的主要使用场合是,针对以非创建对象的方式为对象分配内存的情况(即使用本地方法调用非Java代码,如C的malloc函数),如果内存得不到释放,可以在finalize方法中调用相应的函数来释放内存(如C的free函数)。

总的来说,如果是要进行非释放内存的清理工作,必须专门为此定义一个普通方法,并显式调用该普通方法才可以。

finalize的特殊用法——对象终结条件的验证
可以在finalize方法中,加入这样的逻辑,即判断该对象是否需要终结,如果不满足终结条件,可以进行清理工作。虽然finalize方法不一定会执行,但还是能作为最后一道防线。例如:

/**
* 使用finialize()来检验没有被正确清理的对象
*/

public class TerminationCondition {

public static void main(String[] args){
Book novel = new Book(true);
//正确的清理
novel.checkIn();
//忘记清理
new Book(true);
//强制进行垃圾回收
System.gc();
}
}

class Book{

boolean checkedOut = false;

Book(boolean checkOut){
checkedOut = checkOut;
}

void checkIn(){
checkedOut = false;
}

protected void finalize() throws Throwable {
if(checkedOut){
System.out.println("Error: checked out");
}
super.finalize();
}

}

垃圾回收器

引用计数

每个对象都有一个引用计数器,当有引用连接至对象时,引用计数加1;当引用离开作用域或被置为null时,引用计数减1。当发生垃圾回收时,垃圾回收器会遍历所有对象,当发现某个对象的引用计数为0时则释放其内存。

这一计数的缺陷在于循环引用的情况。例如:
public class Test{

public static void main(String[] args){
A newA = new A();
B newB = new B();
newA.b = newB;
newB.a = newA;
}
}

class A{
B b;
}
class B{
A a;
}
在上述代码中,要释放newB,就必须先释放newA。同理,要释放newA,就必须先释放newB,这就导致对象应该被回收而引用计数不为0的情况。

停止-复制

这一技术基于所有的对象都能被追溯到其存活于堆栈或静态存储区的引用。这一技术从堆栈和静态存储区开始遍历所有的引用,对于每一个引用,必须追踪到其所指向的对象,如果该对象中又包含有引用,则继续追踪这些引用指向的对象。这种方式能所有访问的对象都是有引用指向的(存活对象),因此剩下来的对象就是垃圾了。

该技术将存活对象从旧堆复制到新堆,并使对象连续并紧密排列。而原来存在与旧堆之上的对象则作为垃圾被清理释放。

这一技术有两个缺陷,首先就是维护比实际多一倍的空间,其次是复制,如果程序只产生少量垃圾或者没有垃圾,复制所有存活对象显然就非常的消耗性能。

标记-清扫

这一技术同样是基于所有的对象都能被追溯到其存活于堆栈或静态存储区的引用。同样从堆栈和静态存储区出发,找出所有存活对象。不同的是,每找到一个存活对象,就给它打上一个标记。当找到所有存活对象后,开始清理没有标记的对象。

这一技术的缺陷在于剩下的堆空间是不连续的,如果想获得连续的堆空间的话,就必须对存货对象进行整理。

内存分配以较大的块为单位,对象较大则占用单独的块。有了块之后,垃圾回收器就可以在回收的时候向废弃的块中复制对象。每一个块都有相应的代数(generation count)来记录它是否存活,如果块被某处引用,其代数会增加。垃圾回收器会对上次回收动作之后新分配的块进行整理。这样,大型对象仍然不会复制,只是代数增加,而内含小型对象的块则被复制并整理。

自适应

JVM会监视,如果所有对象都很稳定,垃圾回收器的效率降低的话,则切换到“标记-清扫”方式;而如果堆空间中出现很多碎片就会切换回“停止-复制”方式。

综合来说就是“自适应的、分代的、停止-复制、标记-清扫”式的垃圾回收器。