Skip to content

【OnJava-六】初始化和清理

“不安全” 的编程是造成编程代价昂贵的罪魁祸首之一。有两个安全性问题:初始化和清理。C 语言中很多的 bug 都是因为程序员忘记初始化导致的。尤其是很多类库的使用者不知道如何初始化类库组件,甚至他们必须得去初始化。清理则是另一个特殊的问题,因为当你使用一个元素做完事后就不会去关心这个元素,所以你很容易忘记清理它。这样就造成了元素使用的资源滞留不会被回收,直到程序消耗完所有的资源(特别是内存)。

C++ 引入了构造器的概念,这是一个特殊的方法,每创建一个对象,这个方法就会被自动调用。Java 采用了构造器的概念,另外还使用了垃圾收集器(Garbage Collector, GC)去自动回收不再被使用的对象所占的资源。

利用构造器保证初始化

你可能想为每个类创建一个 initialize() 方法,该方法名暗示着在使用类之前需要先调用它。不幸的是,用户必须得记得去调用它。在 Java 中,类的设计者通过构造器保证每个对象的初始化。如果一个类有构造器,那么 Java 会在用户使用对象之前(即对象刚创建完成)自动调用对象的构造器方法,从而保证初始化。

java
public class Main {  
    static class Rock {  
        // 无参构造器
        Rock() {  
            System.out.print("Rock ");  
        }  
        // 有参数构造器
        //Rock2(int i) {
	    //    System.out.print("Rock " + i + " ");
	    //}
    }      
      
    public static void main(String[] args) {  
        for (int i = 0; i < 10; i++) {  
            new Rock();  
        }  
    }  
}
// out: Rock Rock Rock Rock Rock Rock Rock Rock Rock Rock
public class Main {  
    static class Rock {  
        // 无参构造器
        Rock() {  
            System.out.print("Rock ");  
        }  
        // 有参数构造器
        //Rock2(int i) {
	    //    System.out.print("Rock " + i + " ");
	    //}
    }      
      
    public static void main(String[] args) {  
        for (int i = 0; i < 10; i++) {  
            new Rock();  
        }  
    }  
}
// out: Rock Rock Rock Rock Rock Rock Rock Rock Rock Rock
  • 构造器方法名与类名相同,不需要符合首字母小写的编程风格
  • 是特殊的方法,无返回值
  • 如果某方法是唯一的构造器,那么编译器不允许你以其他任何方式创建对象
  • 如果你创建一个类,类中没有构造器,那么编译器就会自动为你创建一个无参构造器

方法重载

  • 构造器重载
  • 被重载的方法必须有独立无二的参数列表

this关键字

  • this 关键字只用在一些必须显式使用当前对象引用的特殊场合。例如,用在 return 语句中返回对当前对象的引用。

    java
    public class Leaf {
    	int i = 0;
    	Leaf increment() {
    		i++;
    		return this;
    	}
    	void print() {
    		System.out.println("i = " + i);
    	}
    	public static void main(String[] args) {
    		Leaf x = new Leaf();
    		x.increment().increment().increment().print();
    	}
    }
    public class Leaf {
    	int i = 0;
    	Leaf increment() {
    		i++;
    		return this;
    	}
    	void print() {
    		System.out.println("i = " + i);
    	}
    	public static void main(String[] args) {
    		Leaf x = new Leaf();
    		x.increment().increment().increment().print();
    	}
    }
  • this 关键字在向其他方法传递当前对象时也很有用:

    java
    class Person {
        public void eat(Apple apple) {
            Apple peeled = apple.getPeeled();
            System.out.println("Yummy");
        }
    }
    public class Peeler {
        static Apple peel(Apple apple) {
            return apple; 
        }
    }
    public class Apple {
        Apple getPeeled() {
            return Peeler.peel(this);
        }
    }
    public class PassingThis {
        public static void main(String[] args) {
            new Person().eat(new Apple());
        }
    }
    class Person {
        public void eat(Apple apple) {
            Apple peeled = apple.getPeeled();
            System.out.println("Yummy");
        }
    }
    public class Peeler {
        static Apple peel(Apple apple) {
            return apple; 
        }
    }
    public class Apple {
        Apple getPeeled() {
            return Peeler.peel(this);
        }
    }
    public class PassingThis {
        public static void main(String[] args) {
            new Person().eat(new Apple());
        }
    }
  • 在构造器中调用构造器: 当你在一个类中写了多个构造器,有时你想在一个构造器中调用另一个构造器来避免代码重复。你通过 this 关键字实现这样的调用。

    java
    public class Flower {
        int petalCount = 0;
        String s = "initial value";
        Flower(int petals) {
            petalCount = petals;
            System.out.println("Constructor w/ int arg only, petalCount = " + petalCount);
        }
        Flower(String ss) {
            System.out.println("Constructor w/ string arg only, s = " + ss);
            s = ss;
        }
        Flower(String s, int petals) {
            this(petals);
            // this避免混淆
            this.s = s; 
            System.out.println("String & int args");
        }
        Flower() {
            this("hi", 47);
            System.out.println("no-arg constructor");
        }
        void printPetalCount() {
            System.out.println("petalCount = " + petalCount + " s = " + s);
        }
        public static void main(String[] args) {
            Flower x = new Flower();
            x.printPetalCount();
        }
    }
    public class Flower {
        int petalCount = 0;
        String s = "initial value";
        Flower(int petals) {
            petalCount = petals;
            System.out.println("Constructor w/ int arg only, petalCount = " + petalCount);
        }
        Flower(String ss) {
            System.out.println("Constructor w/ string arg only, s = " + ss);
            s = ss;
        }
        Flower(String s, int petals) {
            this(petals);
            // this避免混淆
            this.s = s; 
            System.out.println("String & int args");
        }
        Flower() {
            this("hi", 47);
            System.out.println("no-arg constructor");
        }
        void printPetalCount() {
            System.out.println("petalCount = " + petalCount + " s = " + s);
        }
        public static void main(String[] args) {
            Flower x = new Flower();
            x.printPetalCount();
        }
    }

this && static

记住了 this 关键字的内容,你会对 static 修饰的方法有更加深入的理解:
==static 方法中不会存在 this。你不能在静态方法中调用非静态方法(反之可以)。

静态方法是为类而创建的,不需要任何对象。事实上,这就是静态方法的主要目的,静态方法看起来就像全局方法一样,但是 Java 中不允许全局方法,一个类中的静态方法可以被其他的静态方法和静态属性访问。

一些人认为静态方法不是面向对象的,因为它们的确具有全局方法的语义。使用静态方法,因为不存在 this,所以你没有向一个对象发送消息。的确,如果你发现代码中出现了大量的 static 方法,就该重新考虑自己的设计了。然而,static 的概念很实用,许多时候都要用到它。至于它是否真的” 面向对象”,就留给理论家去讨论吧。

java
// 你不能在静态方法中调用非静态方法(
public class Main {  
    static void staticFunc() {  
       this.axx();    // 编译错误  
    }  
    void axx() {  
        staticFunc(); // work  
    }  
}
// 你不能在静态方法中调用非静态方法(
public class Main {  
    static void staticFunc() {  
       this.axx();    // 编译错误  
    }  
    void axx() {  
        staticFunc(); // work  
    }  
}

垃圾回收器

Java 中有垃圾回收器回收无用对象占用的内存。

但现在考虑一种特殊情况:你创建的对象不是通过 new 来分配内存的,而垃圾回收器只知道如何释放用 new 创建的对象的内存,所以它不知道如何回收不是 new 分配的内存。为了处理这种情况,Java 允许在类中定义一个名为 finalize() 的方法。

  • finalize() 是一个潜在的编程陷阱,因为一些程序员(尤其是 C++ 程序员)会一开始把它误认为是 C++ 中的析构函数(C++ 在销毁对象时会调用这个函数)。所以有必要明确区分一下:在 C++ 中,对象总是被销毁的(在一个 bug-free 的程序中),而在 Java 中,对象并非总是被垃圾回收,或者换句话说:
    • 1 对象可能不被垃圾回收。
    • 2 垃圾回收不等同于析构。
      1. 垃圾回收只与内存有关。

这意味着在你不再需要某个对象之前,如果必须执行某些动作,你得自己去做。Java 没有析构器或类似的概念,所以你必须得自己创建一个普通的方法完成这项清理工作。例如,对象在创建的过程中会将自己绘制到屏幕上。如果不是明确地从屏幕上将其擦除,它可能永远得不到清理。如果在 finalize() 方法中加入某种擦除功能,那么当垃圾回收发生时,finalize() 方法被调用(不保证一定会发生),图像就会被擦除,要是” 垃圾回收” 没有发生,图像则仍会保留下来。

也许你会发现,只要程序没有濒临内存用完的那一刻,对象占用的空间就总也得不到释放。如果程序执行结束,而垃圾回收器一直没有释放你创建的任何对象的内存,则当程序退出时,那些资源会全部交还给操作系统。这个策略是恰当的,因为垃圾回收本身也有开销,要是不使用它,那就不用支付这部分开销了。

finalize() 的用途

如果你不能将 finalize() 作为通用的清理方法,那么这个方法有什么用呢?

这引入了要记住的第 3 点:

垃圾回收只与内存有关。

也就是说,使用垃圾回收的唯一原因就是为了回收程序不再使用的内存。所以对于与垃圾回收有关的任何行为来说(尤其是 finalize() 方法),它们也必须同内存及其回收有关。

当对某个对象不感兴趣时——也就是它将被清理了,这个对象应该处于某种状态,这种状态下它占用的内存可以被安全地释放掉。例如,如果对象代表了一个打开的文件,在对象被垃圾回收之前程序员应该关闭这个文件。只要对象中存在没有被适当清理的部分,程序就存在很隐晦的 bug。finalize() 可以用来最终发现这个情况,尽管它并不总是被调用。如果某次 finalize() 的动作使得 bug 被发现,那么就可以据此找出问题所在——这才是人们真正关心的。以下是个简单的例子,示范了 finalize() 的可能使用方式:

java
import onjava.*;
class Book {
    boolean checkedOut = false;
    Book(boolean checkOut) {
        checkedOut = checkOut;
    }
    void checkIn() {
        checkedOut = false;
    }
    @Override
    protected void finalize() throws Throwable {
        if (checkedOut) {
            System.out.println("Error: checked out");
        }
    }
}
public class TerminationCondition {
    public static void main(String[] args) {
        Book novel = new Book(true);
        novel.checkIn();
        new Book(true);
        // 用于强制进行终结动作
        System.gc();
        new Nap(1); 
    }
}
import onjava.*;
class Book {
    boolean checkedOut = false;
    Book(boolean checkOut) {
        checkedOut = checkOut;
    }
    void checkIn() {
        checkedOut = false;
    }
    @Override
    protected void finalize() throws Throwable {
        if (checkedOut) {
            System.out.println("Error: checked out");
        }
    }
}
public class TerminationCondition {
    public static void main(String[] args) {
        Book novel = new Book(true);
        novel.checkIn();
        new Book(true);
        // 用于强制进行终结动作
        System.gc();
        new Nap(1); 
    }
}

https://juejin.cn/post/7033382936112496653
finalize() 方法是在对象被回收之前调用的方法。但是 finalize() 方法的调用具有不确定性。
当进行完可达性分析之后,某个对象被标记为不可达时,会判断当前对象是否重写了 finalize() 方法,是否已经执行过对象的 finalize() 方法,如果没有覆盖,或者已经执行过,则不再执行,对象将被回收。
如果有必要执行 finalize() 方法,则会将这个对象放到一个 F-Queue 的低优先级队列里等待执行。之后 GC 将对 F-Queue 中的对象再次进行可达性分析。
所以,如果对象可以在 finalize() 方法中再次复活,即将自己的 this 指针,重新赋值到某个可达的对象的引用上。

垃圾回收器如何工作

如果你以前用过的语言,在堆上分配对象的代价十分高昂,你可能自然会觉得 Java 中所有对象(基本类型除外)在堆上分配的方式也十分高昂。然而,垃圾回收器能很明显地提高对象的创建速度。这听起来很奇怪——存储空间的释放影响了存储空间的分配,但这确实是某些 Java 虚拟机的工作方式。这也意味着,Java 从堆空间分配的速度可以和其他语言在栈上分配空间的速度相媲美

  • 其他系统中的垃圾回收机制
    • 引用计数: 存在循环引用问题
  • 可达性分析算法
    • 通过一系列称之为“GC Roots”的对象作为起始节点,通过这些节点搜索所有可达的节点。当发现某些对象不可达,即说明此对象不可用。
    • GC Roots 的对象
      • 虚拟机栈中引用的对象
      • 方法区中类静态属性引用的对象
      • 方法区中常量引用的对象

在更快的策略中,垃圾回收器并非基于引用计数。它们依据的是:对于任意” 活” 的对象,一定能最终追溯到其存活在栈或静态存储区中的引用。这个引用链条可能会穿过数个对象层次,由此,如果从栈或静态存储区出发,遍历所有的引用,你将会发现所有” 活” 的对象。对于发现的每个引用,必须追踪它所引用的对象,然后是该对象包含的所有引用,如此反复进行,直到访问完” 根源于栈或静态存储区的引用” 所形成的整个网络。你所访问过的对象一定是” 活” 的。注意,这解决了对象间循环引用的问题,这些对象不会被发现,因此也就被自动回收了。

在这种方式下,Java 虚拟机采用了一种 自适应 的垃圾回收技术。至于如何处理找到的存活对象,取决于不同的 Java 虚拟机实现。其中有一种做法叫做停止 - 复制(stop-and-copy)。顾名思义,这需要先暂停程序的运行(不属于后台回收模式),然后将所有存活的对象从当前堆复制到另一个堆,没有复制的就是需要被垃圾回收的。另外,当对象被复制到新堆时,它们是一个挨着一个紧凑排列,然后就可以按照前面描述的那样简单、直接地分配新空间了。

当对象从一处复制到另一处,所有指向它的引用都必须修正。位于栈或静态存储区的引用可以直接被修正,但可能还有其他指向这些对象的引用,它们在遍历的过程中才能被找到(可以想象成一个表格,将旧地址映射到新地址)。

这种所谓的” 复制回收器” 效率低下主要因为两个原因。

  • 其一:得有两个堆,然后在这两个分离的堆之间来回折腾,得维护比实际需要多一倍的空间。某些 Java 虚拟机对此问题的处理方式是,按需从堆中分配几块较大的内存,复制动作发生在这些大块内存之间。
  • 其二在于复制本身。一旦程序进入稳定状态之后,可能只会产生少量垃圾,甚至没有垃圾。尽管如此,复制回收器仍然会将所有内存从一处复制到另一处,这很浪费。为了避免这种状况,一些 Java 虚拟机会进行检查:要是没有新垃圾产生,就会转换到另一种模式(即” 自适应”)。这种模式称为标记 - 清扫(mark-and-sweep),Sun 公司早期版本的 Java 虚拟机一直使用这种技术。对一般用途而言,” 标记 - 清扫” 方式速度相当慢,但是当你知道程序只会产生少量垃圾甚至不产生垃圾时,它的速度就很快了。
  • “标记 - 清扫” 所依据的思路仍然是从栈和静态存储区出发,遍历所有的引用,找出所有存活的对象。但是,每当找到一个存活对象,就给对象设一个标记,并不回收它。只有当标记过程完成后,清理动作才开始。在清理过程中,没有标记的对象将被释放,不会发生任何复制动作。” 标记 - 清扫” 后剩下的堆空间是不连续的,垃圾回收器要是希望得到连续空间的话,就需要重新整理剩下的对象。
  • “停止 - 复制” 指的是这种垃圾回收动作不是在后台进行的;相反,垃圾回收动作发生的同时,程序将会暂停。在 Oracle 公司的文档中会发现,许多参考文献将垃圾回收视为低优先级的后台进程,但是早期版本的 Java 虚拟机并不是这么实现垃圾回收器的。当可用内存较低时,垃圾回收器会暂停程序。同样,” 标记 - 清扫” 工作也必须在程序暂停的情况下才能进行。

如前文所述,这里讨论的 Java 虚拟机中,内存分配以较大的” 块” 为单位。如果对象较大,它会占用单独的块。严格来说,” 停止 - 复制” 要求在释放旧对象之前,必须先将所有存活对象从旧堆复制到新堆,这导致了大量的内存复制行为。有了块,垃圾回收器就可以把对象复制到废弃的块。每个块都有年代数来记录自己是否存活。通常,如果块在某处被引用,其年代数加 1,垃圾回收器会对上次回收动作之后新分配的块进行整理。这对处理大量短命的临时对象很有帮助。垃圾回收器会定期进行完整的清理动作——大型对象仍然不会复制(只是年代数会增加),含有小型对象的那些块则被复制并整理。Java 虚拟机会监视,如果所有对象都很稳定,垃圾回收的效率降低的话,就切换到” 标记 - 清扫” 方式。同样,Java 虚拟机会跟踪” 标记 - 清扫” 的效果,如果堆空间出现很多碎片,就会切换回” 停止 - 复制” 方式。这就是” 自适应” 的由来,你可以给它个啰嗦的称呼:” 自适应的、分代的、停止 - 复制、标记 - 清扫” 式的垃圾回收器。

Java 虚拟机中有许多附加技术用来提升速度。尤其是与加载器操作有关的,被称为” 即时”(Just-In-Time, JIT)编译器的技术。这种技术可以把程全部或部分翻译成本地机器码,所以不需要 JVM 来进行翻译,因此运行得更快。当需要装载某个类(通常是创建该类的第一个对象)时,编译器会先找到其 .class 文件,然后将该类的字节码装入内存。你可以让即时编译器编译所有代码,但这种做法有两个缺点:一是这种加载动作贯穿整个程序生命周期内,累加起来需要花更多时间;二是会增加可执行代码的长度(字节码要比即时编译器展开后的本地机器码小很多),这会导致页面调度,从而一定降低程序速度。另一种做法称为_惰性评估_,意味着即时编译器只有在必要的时候才编译代码。这样,从未被执行的代码也许就压根不会被 JIT 编译。新版 JDK 中的 Java HotSpot 技术就采用了类似的做法,代码每被执行一次就优化一些,所以执行的次数越多,它的速度就越快。

成员初始化

构造器初始化

java
class Window {
    Window(int marker) {
        System.out.println("Window(" + marker + ")");
    }
}

class House {
    Window w1 = new Window(1); 
    Window w2 = new Window(2);     
    Window w3 = new Window(3); 
    
    House() {
        System.out.println("House()");
        w3 = new Window(33); 
    }

    void f() {
        System.out.println("f()");
    }

}
public class OrderOfInitialization {
    public static void main(String[] args) {
        House h = new House();
        h.f(); 
    }
}


//Window(1)
//Window(2)
//Window(3)
//House()
//Window(33)
//f()
class Window {
    Window(int marker) {
        System.out.println("Window(" + marker + ")");
    }
}

class House {
    Window w1 = new Window(1); 
    Window w2 = new Window(2);     
    Window w3 = new Window(3); 
    
    House() {
        System.out.println("House()");
        w3 = new Window(33); 
    }

    void f() {
        System.out.println("f()");
    }

}
public class OrderOfInitialization {
    public static void main(String[] args) {
        House h = new House();
        h.f(); 
    }
}


//Window(1)
//Window(2)
//Window(3)
//House()
//Window(33)
//f()

w3 被初始化了两次:
一次在调用构造器前
一次在构造器调用期间(第一次引用的对象将被丢弃,并作为垃圾回收)

静态数据的初始化

无论创建多少个对象,静态数据都只占用一份存储区域。
- static 关键字不能应用于局部变量,所以只能作用于属性(字段、域)。
- 初始值
- 如果一个字段是静态的基本类型,你没有初始化它,那么它就会获得基本类型的标准初值
- 如果它是对象引用,那么它的默认初值就是 null

调用顺序总结
- 初始化static成员变量, 只初始化一次。只有在必要时刻才会进行。
- 这个class进行初始化 、直接调用static 都会初始化这个类内部的全部静态变量
- 初始化非static 成员变量
- 构造方法

java
class Bowl {  
    Bowl(int marker) {  
        System.out.println("Bowl(" + marker + ")");  
    }  
  
    void f1(int marker) {  
        System.out.println("f1(" + marker + ")");  
    }  
}  
  
class Table {  
    static Bowl bowl1 = new Bowl(1);  
    static Bowl bowl2 = new Bowl(2);  
  
    Table() {  
        System.out.println("Table()");  
        bowl2.f1(1);  
    }  
  
    void f2(int marker) {  
        System.out.println("f2(" + marker + ")");  
    }  
}  
  
class Cupboard {  
    static Bowl bowl4 = new Bowl(4);  
    static Bowl bowl5 = new Bowl(5);  
  
    Bowl bowl3 = new Bowl(3);  
  
    Cupboard() {  
        System.out.println("Cupboard()");  
        bowl4.f1(2);  
    }  
  
    void f3(int marker) {  
        System.out.println("f3(" + marker + ")");  
    }  
}  
  
public class Main {  
    static Table table = new Table();  
    static Cupboard cupboard = new Cupboard();  
  
    public static void main(String[] args) {  
        System.out.println("main creating new Cupboard()");  
        new Cupboard();  
// ---- static的优先初始化: static table ----//        Bowl(1)  
//        Bowl(2)  
//        Table()  
//        f1(1)    ---- table的构造方法中调用 ---  
// ---- static的优先初始化: static cupboard ----//        Bowl(4)  
//        Bowl(5)  
//        Bowl(3)    ---- 非静态成员变量初始化  
//        Cupboard()  
//        f1(2)  ---- Cupboard的构造方法中调用 ---  
//        main creating new Cupboard()  - -main方法 --//        Bowl(3)                    ---- 非静态成员变量初始化 ,静态不再初始化  
//        Cupboard()  
//        f1(2)  
    }  
}
class Bowl {  
    Bowl(int marker) {  
        System.out.println("Bowl(" + marker + ")");  
    }  
  
    void f1(int marker) {  
        System.out.println("f1(" + marker + ")");  
    }  
}  
  
class Table {  
    static Bowl bowl1 = new Bowl(1);  
    static Bowl bowl2 = new Bowl(2);  
  
    Table() {  
        System.out.println("Table()");  
        bowl2.f1(1);  
    }  
  
    void f2(int marker) {  
        System.out.println("f2(" + marker + ")");  
    }  
}  
  
class Cupboard {  
    static Bowl bowl4 = new Bowl(4);  
    static Bowl bowl5 = new Bowl(5);  
  
    Bowl bowl3 = new Bowl(3);  
  
    Cupboard() {  
        System.out.println("Cupboard()");  
        bowl4.f1(2);  
    }  
  
    void f3(int marker) {  
        System.out.println("f3(" + marker + ")");  
    }  
}  
  
public class Main {  
    static Table table = new Table();  
    static Cupboard cupboard = new Cupboard();  
  
    public static void main(String[] args) {  
        System.out.println("main creating new Cupboard()");  
        new Cupboard();  
// ---- static的优先初始化: static table ----//        Bowl(1)  
//        Bowl(2)  
//        Table()  
//        f1(1)    ---- table的构造方法中调用 ---  
// ---- static的优先初始化: static cupboard ----//        Bowl(4)  
//        Bowl(5)  
//        Bowl(3)    ---- 非静态成员变量初始化  
//        Cupboard()  
//        f1(2)  ---- Cupboard的构造方法中调用 ---  
//        main creating new Cupboard()  - -main方法 --//        Bowl(3)                    ---- 非静态成员变量初始化 ,静态不再初始化  
//        Cupboard()  
//        f1(2)  
    }  
}

测试2

java
public class Main {  
    public static void main(String[] args) {  
        Bowl bowl1 = Table.bowl1;  
        // 输出:
//        Bowl(1)  
//        Bowl(2)  
    }  
}
public class Main {  
    public static void main(String[] args) {  
        Bowl bowl1 = Table.bowl1;  
        // 输出:
//        Bowl(1)  
//        Bowl(2)  
    }  
}
java
public class Main {  
    public static void main(String[] args) {  
        Table table = new Table();  
// 输出:
//        Bowl(1)  
//        Bowl(2)  
//        Table()  
//        f1(1)  
    }  
}
public class Main {  
    public static void main(String[] args) {  
        Table table = new Table();  
// 输出:
//        Bowl(1)  
//        Bowl(2)  
//        Table()  
//        f1(1)  
    }  
}

静态初始化只有在必要时刻才会进行

  • 如果不创建 Table 对象,也不引用 Table.bowl1 或 Table.bowl2,那么静态的 Bowl 类对象 bowl1 和 bowl2 永远不会被创建。
  • 只有在第一个 Table 对象被创建(或被访问)时,它们才会被初始化。此后,静态对象不会再次被初始化。

概括一下创建对象的过程,假设有个名为 Dog 的类:

  1. 即使没有显式地使用 static 关键字,构造器实际上也是静态方法。所以,当首次创建 Dog 类型的对象时,或是首次访问 Dog 类的静态方法或属性,Java 解释器必须在类路径中查找,以定位 Dog.class
  2. 当加载完 Dog.class 后(后面会学到,这将创建一个 Class 对象),有关静态初始化的所有动作都会执行。因此,静态初始化只会在首次加载 Class 对象时初始化一次
  3. 当用 new Dog() 创建对象时,首先会在堆上为 Dog 对象分配足够的存储空间。
  4. 分配的存储空间首先会被清零,即会将 Dog 对象中的所有基本类型数据设置为默认值(数字会被置为 0,布尔型和字符型也相同),引用被置为 null
  5. 执行所有出现在字段定义处的初始化动作。
  6. 执行构造器。你将会在” 复用” 这一章看到,这可能会牵涉到很多动作,尤其当涉及继承的时候。

显式的静态初始化

你可以将一组静态初始化动作放在类里面一个特殊的” 静态子句”(有时叫做静态块)中。像下面这样:

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

非静态实例初始化

Java 提供了被称为_实例初始化_的类似语法,用来初始化每个对象的非静态变量,例如:

java
class Mug {
    Mug(int marker) {
        System.out.println("Mug(" + marker + ")");
    }
}
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()");
    }
    Mugs(int i) {
        System.out.println("Mugs(int)");
    }
    public static void main(String[] args) {
        System.out.println("Inside main()");
        new Mugs();
        System.out.println("new Mugs() completed");
        new Mugs(1);
        System.out.println("new Mugs(1) completed");
    }
}

输出:

Inside main
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs()
new Mugs() completed
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs(int)
new Mugs(1) completed
class Mug {
    Mug(int marker) {
        System.out.println("Mug(" + marker + ")");
    }
}
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()");
    }
    Mugs(int i) {
        System.out.println("Mugs(int)");
    }
    public static void main(String[] args) {
        System.out.println("Inside main()");
        new Mugs();
        System.out.println("new Mugs() completed");
        new Mugs(1);
        System.out.println("new Mugs(1) completed");
    }
}

输出:

Inside main
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs()
new Mugs() completed
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs(int)
new Mugs(1) completed

看起来它很像静态代码块,只不过少了 static 关键字。这种语法对于支持” 匿名内部类”(参见” 内部类” 一章)的初始化是必须的,但是你也可以使用它保证某些操作一定会发生,而不管哪个构造器被调用。从输出看出,示例初始化子句是在两个构造器之前执行的。

数组初始化

java
int[] a1 = {1, 2, 3, 4, 5};  
for (int i = 0; i < a1.length; i++) {  
    a1[i] += 1;  
}
int[] a1 = {1, 2, 3, 4, 5};  
for (int i = 0; i < a1.length; i++) {  
    a1[i] += 1;  
}

动态数组

java
import java.util.Arrays;  
import java.util.Random;  
  
public class Main {  
    public static void main(String[] args) {  
        Random rand = new Random(47);  
        Integer[] a = new Integer[rand.nextInt(20)];  
        System.out.println("length of a = " + a.length);  
        for (int i = 0; i < a.length; i++) {  
            a[i] = rand.nextInt(500);  
        }  
        System.out.println(Arrays.toString(a));  
    }  
}
import java.util.Arrays;  
import java.util.Random;  
  
public class Main {  
    public static void main(String[] args) {  
        Random rand = new Random(47);  
        Integer[] a = new Integer[rand.nextInt(20)];  
        System.out.println("length of a = " + a.length);  
        for (int i = 0; i < a.length; i++) {  
            a[i] = rand.nextInt(500);  
        }  
        System.out.println(Arrays.toString(a));  
    }  
}

先初始化了大小,再进行赋值。

可变参数列表

java
void printArray(Object... args) {
	for (Object obj: args) {
		System.out.print(obj + " ");
	}
	System.out.println();
}
void printArray(Object... args) {
	for (Object obj: args) {
		System.out.print(obj + " ");
	}
	System.out.println();
}

枚举类型

  • 默认会创建toString方法
  • 默认创建ordinal方法
  • 可在switch语句中使用
java
enum Spiciness {  
    NOT, MILD, MEDIUM, HOT, FLAMING  
}  
  
public class Main {  
    public static void main(String[] args) {  
        Spiciness howHot = Spiciness.MEDIUM;  
        // 枚举  
        // 默认会创建toString方法  
        // 默认会创建 ordinal        System.out.println(howHot);  
        System.out.println(howHot.toString());  
        System.out.println(howHot.ordinal());  
  
        for (Spiciness value : Spiciness.values()) {  
            System.out.println(value + ":" + value.ordinal());  
        }  
		// NOT:0
		// MILD:1
		// MEDIUM:2
		// HOT:3
		// FLAMING:4        
    }  
}
enum Spiciness {  
    NOT, MILD, MEDIUM, HOT, FLAMING  
}  
  
public class Main {  
    public static void main(String[] args) {  
        Spiciness howHot = Spiciness.MEDIUM;  
        // 枚举  
        // 默认会创建toString方法  
        // 默认会创建 ordinal        System.out.println(howHot);  
        System.out.println(howHot.toString());  
        System.out.println(howHot.ordinal());  
  
        for (Spiciness value : Spiciness.values()) {  
            System.out.println(value + ":" + value.ordinal());  
        }  
		// NOT:0
		// MILD:1
		// MEDIUM:2
		// HOT:3
		// FLAMING:4        
    }  
}