5、初始化与清理

在C语言和C++中系统是不会自动清理垃圾,导致初始化的变量一直存在内存中,导致内存泄漏。

所以Java提出了“垃圾回收器”,对于不再使用的内存资源,垃圾回收能自动将其释放。

5.1 构造器(构造方法)确保初始化

对每一个类都需要定义一个initialize()方法,Java用户在操作对象之前会自动调用相应的构造器。

在Java中构造函数的名字和类名是相同的

class Rock{
    Rock(){
        System.out.println("这是构造方法")
    }
    
}

Rock r1 = new Rock(); //构造一个Rock对象
Rock r2 = new Rock(); //再构造一个Rock对象

注!如果不写构造器方法,则使用默认构造器,也就是无参构造器。

当写有参数构造器时,就会覆盖掉之前的无参构造器,所以需要再写一个无参构造器。

class Rock2{
    Rock2(int i ){
        System.out.println("Rock"+i)
    }
}

Rock2 rock2 = new Rock2(5); //有参构造器需要传参

5.2 方法重载

在java中可以使用相同的方法名,但是参数不同。在进行调用时,可以根据不同的参数类型,来调用相应的方法。

重载构造方法

class Tree{
    int height;
    // 重载构造器方法
    Tree(){
        height = 0;
    }
    Tree(int h){
        height = h
    }
    // 重载其他方法
    void info(){
        System.out.println("Tree is" + height);
    }
    void info(String s){
        System.out.println(s + "Tree is " + height);
    }
    
}

5.2.1 区分重载方法

有几个方法有相同的名字,java如何区分需要调用哪一个?

根据参数的数量,参数的类型,甚至参数的顺序来区分。

但是,《Thinking in Java》的作者并不建议用参数的顺序区分,因为这样会使得代码不好维护。

5.2.2 涉及基本类型的重载

基本类型能从一个“较小”的类型自动提升到一个“较大”的类型。

如果此过程涉及重载,就会造成混淆。

较小的数据类型 char、byte、short

较大的数据类型 int

对于int类型,他只会找参数为int的方法

但是char类型不一样,如果它找不到char类型的方法,就会直接提升至int型。

5.2.3 以返回值区分方法

相同的参数,但是不同的返回值是不行的

因为编译器难以区分。

void f(){}
int f(){return 1;}
//这是不可以的

5.3 默认构造器(构造方法)

如果你不写构造器(构造方法),那么编译器会自动创建一个构造方法

但是当你写了有参构造方法后,就会覆盖掉原来的默认构造方法。

5.4 this关键字

如果统一类型有两个对象,分别是a和b。如何才能让这两个对象都能调用peel()方法。

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

...main方法
    Banana a = new Banana();
    Banana b = new Banana();
	
	a.peel(1);
	b.peel(2);
    
//如果只有一个peel()方法,如何知道是a还是b调用的
// Banana.peel(a,1);
// Banana.peel(b,2);
// 以上代码是看不到的,但是编译器会偷偷把对象作为参数传入进去。在对象中就可以使用this关键字来确定当前对象。
public class Leaf{
    int i =0;
    Leaf increment(){
        i ++;
        return this; //返回当前对象
    }
    
    Leaf(int i){
        this.i = i;
    }
}

5.4.1 在构造器(构造方法)中调用构造器(构造方法)

通过this关键字,就可以调用构造方法

注:只可以在构造方法中调用一次this的构造方法

public class Person{
    int height;
    String name;
    
    Person(int height){
        this.height = height;
        this.name = "张三"
    }
    Person(String name){
        this.name = name;
        this(180); //调用当前对象的构造方法
    }
    
    Person(int height,String name){
        this.height = height;
        this.name = name;
    }

    Person(){
        this(180,"张三");
    }
      
}

5.4.2 static的含义

在了解this关键字之后,就能更全面地理解static方法的含义。

加了static之后,这个方法和属性只属于这个类,和某个特定的对象无关。

举个例子:对于Chinese这个类,Chinese类的任何对象(zhangsan、lisi)的国籍都是china,和任何特定的对象无关,所以国籍就是一个static的。

static方法就是没有this的方法,且static方法的内部不能调用非静态方法。

非静态方法,可以调用静态方法。

《Thinking in Java》的作者讨论关于static是否是“面向对象的问题”,因为有人认为static具有全局函数的语义。但是在设计中确实有存在的必要。

5.5 清理:终结处理和垃圾回收

在Java的类中定义一个名为finalize()的方法。

一旦垃圾回收机制准备处理这个对象,就会首先调用finalize()方法

注:finalize() 并不是C++中的析构方法。

5.5.1 finalize()的用途为何?

这里要记住一点:垃圾回收只与内存有关

所以finalize()只和内存释放有关。

但是如果在Java语言中调用非Java(C或者C++)代码,那么可能就涉及到内存问题,所以需要进行手动释放。

5.5.2 你必须实施清理

Java的垃圾回收机制,使得Java并不需要手动的进行释放内存,也不需要析构方法。

但是垃圾回收机制也不是万能的,JVM只有在面临内存耗尽的情况下,才会调用垃圾回收机制。

5.5.3 终结条件

不能指望finalize()

必须创建其他的“清理”方法,并且调用清理方法。使得finalize()的用处并不是很大。

不过finalize()有一个有趣的用法,就是判断是否该对象是否被释放掉。

5.5.4 垃圾回收器如何工作

通常人们认为在堆上分配对象代价很大,但是因为垃圾回收机制,反倒提升了对象的创建速度。

举个例子:把C++里面的堆想象成一个院子,每个对象负责自己的地盘,一段时间后,对象可能被销毁,地盘必须加以宠用。但在Java虚拟机中,堆的实现完全不同,它更像一个传送带,每分配一个对象,就往前移动一格,这就加快了分配速度。Java的“堆指针”只是简单地移动到尚未分配的区域。当然,在实际过程中在薄记工作方面还有少量额外开销,但比不上查询可用空间开销大。

上面的例子是比较粗糙的,Java的堆未必完全像传送带那有工作,若真是传送带,那么势必会导致频繁的内存页面调度(频繁地移出硬盘),页面调度会显著地影响性能,最终当创建了足够多的对象之后,内存将耗尽。其秘密在于垃圾回收器的接入,它工作时一面回收空间,一面使堆中的对象紧凑排列,这样“堆指针”就可以很容易移动到更靠近传送带的开始处,避免了页面错误。

java并没有引入计数器:计数器只的是该对象被多少个指针引用过,但计数器为0使,就调用回收机制,将其回收。但是在存在循环引用的问题,使得对象不能被很好地回收。

java中的回收依据,任何活得对象,必然能追溯到存活在堆栈或者静态区域中的引用。如果从堆栈和静态区开始,遍历所有的引用,必然能找到所有“或”的对象,对于发现的每个引用,必须追踪它所引用的对象,然后使此对象包含的所有引用,如此反复,直到“根源于堆栈和静态区的引用”所形成的网络全部被访问位置。对于没被发现的对象,就将其回收。

如何分开活着的对象和没存活的对象,这取决于不同的Java虚拟机,有一种方法名为停止-复制**的方法,先暂停程序的运行,然后将活的节点复制到另一个堆,没有被复制的全部是垃圾,将其删除。当时把对象搬到另一处,所有指针都必须要修正,这会降低效率。

如何分开活着的对象和没存活的对象,频繁的复制会造成浪费,且程序进入稳定后,只会产生少量垃圾,甚至没有垃圾。还频繁复制会降低效率,所以此时就转换另一种工作模式,标记-清扫 ,标记清扫的速度相当慢,但是只有少量垃圾的时候,效率就很快了。

标记-清扫从堆栈和静态存储区触发,遍历所有引用,找到所有存活的对象,进行标记。标记完成后,删除所有没有标记的数据,这会导致内存不连续。就必须重新整理内存对象。

由于垃圾回收效率很低,所以在内存数量很低的时候,回收机制是不会执行的。

在内存分配中是以”块“为单位,一个块中有很多个对象。在执行停止-复制的时候,就可以往废弃的块中拷贝对象。以块为整体,来判断是否存活。

总结起来,就是 自适应的、分代的、停止-复制、标记-清扫的垃圾回收机制。

Java虚拟机中有许多附加技术以提升速度,尤其于加载器操作有关,被称为JIT编译器技术。这种技术将java代码编译为class文件,直接执行class文件,提高运行效率。

编译有两种:

(1) 编译所有的代码,要花更多的时间,且class文件比较大

(2)只编译需要的代码(lazy evaluation)

5.6 成员初始化

Java尽力保证所有变量都初始化。

如果方法局部变量不初始化,就会导致编译错误。

void f(){
    int i;
    i ++ ; //会报错
}

对于类中的数据,如果不初始化,就会设置一个默认值。如char就设置为0等。

5.6.1 指定初始化

class Depth{ }

public class InitVal2{
    boolean b = true;
    char ch = 'x';
    byte b = 47;
    Depth d = new Depth(); //Depth 是自定义的一个类
}

通过方法来进行初始化

// 正常执行
public class Method2{
    int i = f(); //f是一个方法
    int f(){ return 11; }
}
// 正常执行
public class Method3{
    int i = f(2); //f是一个方法
    int f(int n ){ return n*11; }
}

// 正常执行
public class Method4{
    int i = f();
    int f(){return 11; }
    int j = g(i);
    int g(int n){ return n*10; }
}

// !!!!报错 , 所以顺序对执行也有很大的影响。
public class Method5{
    int j = g(i);//报错
    int i = f();
    int f(){return 11; }
    int g(int n){ return n*10; }
}

5.7 构造器(构造函数)的初始化

构造函数不能阻止初始化的进行,会先将数据初始化,再调用构造函数。

public class Counter{
    int i; //先初始化
    public Counter{
        i = 7;//再调用构造函数
    }
}
//先初始化为0,然后再调用构造函数,设置为7

5.7.1 初始化顺序

变量定义的先后顺序决定了初始化的顺序。

先初始化,再调用构造方法等。


5.7.2 静态数据的初始化

无论创建多少个对象,静态数据只占用一份存储区域。

static关键字不能应用于局部变量。

静态数据也必须初始化,没有初始化则指定默认值。

5.7.3 显式的静态初始化(静态块)

Java允许多个静态初始化动作组织成一个特殊的静态子句,也称为“静态块”

什么时候进行初始化

(1)静态数据在首次生成类的对象时

(2)首次访问静态成员时

public class Spoon{
    static int i;
    static{ //静态块
        i = 47;
    }
}
class Cup{	
	Cup(int i){
		System.out.println("Cup构造方法"+i);
	}
	
	void f(int i) {
		System.out.println(i);
	}
}

class Cups{
	static Cup c1;
	static Cup c2;
	static {
		c1 = new Cup(1);
		c2 = new Cup(2);
	}
	Cups(){
		System.out.println("Cups的构造方法");
	}
	
}

public class TestString {	
	public static void main(String[] args) {	
		System.out.println("还没开始调用");
		Cups.c1.f(99);
		
	}
}
// 输出结果
// 还没开始调用
// Cup构造方法1
// Cup构造方法2
// 99

5.7.4 非静态实例初始化

非静态实例初始化跟静态方法的初始化类似。

5.8 数组初始化

数组是具有相同数据类型的列表。

通过方括号下标操作符[ ]来定义和使用

int[] a1;
//也可以
int a1[];

这两句是一样的。

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

a2 = a1;

注意!这只是复制了一个引用,其本质是操作同一个数组。

public class TestString {	
	public static void main(String[] args) {	
		int[] a1 = { 1, 2, 3, 4, 5 };
		int[] a2;
		a2 = a1;		
		a1[2] = 5;		
		System.out.println(a2[2]);
	}
}
// 输出结果
// 5

如果操作数组下标,那么就会抛出数据越界的异常。

int[] a = new int[10];//初始化一个长度为10的数组

5.8.1 可变参数列表

应用于参数个数未知的场合。

由于Object是所有类型的父类,所以可以创建Object类型的数组。

Object[] list = new Object[]{
    new Integer(5),new Float(3.14),new Double(11.11)
};

给方法添加可变参数

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(1,3,5);
        printArray(new Integer(2),new Float(1.1));
    }
}
// 输出结果
// 1, 3, 5, 
// 2, 1.1,

可变参数前也可以添加其他类型的参数

static void printArray(int a,Object... args){
        for(Object obj : args){
            System.out.print(obj + ", ");
        }
        System.out.println();
    }
	
	public static void main(String[] args){
        printArray(1,3,5);
    }

5.9 枚举类型

在JavaSE5之后,添加了枚举类型。

enum Spiciness{
    MOT,MILD,MEDIUM,HOT,FLAMING
}

public class TestString {	
	public static void main(String[] args){
		Spiciness howHot = Spiciness.MEDIUM;
		System.out.println(howHot);
    }	
}

在创建enum时,编译器会自动添加一些特性。比如

(1)创建toString()方法。

(2)创建ordinal()方法,显示所在的下标。

enum Spiciness{
    MOT,MILD,MEDIUM,HOT,FLAMING
}

public class TestString {	
	public static void main(String[] args){
		for(Spiciness s : Spiciness.values()) {
			System.out.println(s+" ordinal " + s.ordinal());
		}
    }	
}
// 输出结果
// MOT ordinal 0
// MILD ordinal 1
// MEDIUM ordinal 2
// HOT ordinal 3
// FLAMING ordinal 4

enum可以在switch语句中使用

switch(degree){
    case MOT: ..... ; break;
    case MILD: .... ; break;
    .....
        default: ....;
}

5.10 总结

C++的发明人在设计C++期间,发现大量的编程错误来自于不正确的初始化。