初识Java 4-1 初始化与清理

目录

通过构造器进行初始化

无参构造器

方法的重载

使用基本类型的重载

this关键字

在构造器中调用构造器

static的含义

 成员初始化

初始化顺序

静态数据的初始化

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

非静态实例的初始化

数组初始化

动态数组的创建

可变参数列表

清理

finalize()的特殊用法

垃圾收集器的工作原理

枚举类型

局部变量类型判断


本笔记参考自: 《On Java 中文版》


        初始化清理的不到位,很容易导致编程上的“不安全”。Java引入了构造器,以此来保证对象的初始化,同时通过垃圾收集器来回收内存的资源。

通过构造器进行初始化

        Java中,类的设计者可以通过编写构造器来确保每一个对象的初始化。当一个类中存在构造器时,Java就会在创建该对象时自动调用它。而为了防止构造器的命名与类中的成员冲突,Java规定:构造器的名字就是类的名字。例如:

class Cla {
    Cla() { // 和类名一致,这就是一个构造器
        System.out.println("这是一条信息,表示构造器被执行。");
    }
}

public class SimpleConstructor {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++)
        new Cla();
    }
}

        程序运行的结果如下:

        上述程序使用循环创建对象,当语句new Cla();被执行时,会发生两件事:① 为这一对象分配空间;② 调用这个类的构造器。Java通过构造器保证一个对象在被使用之前已经被正确地初始化了。

     构造器的命名就是类名,是大小写敏感的。

        构造器可以被分为两种:

  1. 默认构造器:也称为零参数构造器,这种构造器不带有参数;
  2. 含参构造器:和方法类似,构造器也可以通过传入参数来指定创建对象的方式。这种构造器一般由程序员编写。

        使用含参数的构造器的方式与上述例子类似:

class Cla2 {
    Cla2(int i) {
        System.out.println("这是第" + (i + 1) + "次的初始化");
    }
}

public class SimpleConstructor2 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++)
            new Cla2(i);
    }
}

        程序运行的结果是:

    若一个对象只有唯一的一个构造器,那么编译器不会运行通过任何其他方法进行对象的创建。

        注意:在Java中,创建和初始化是一个统一的概念,二者是缺一不可的。

        构造器是一种特殊的方法,这种方法没有返回类型。这和返回类型为空(void)是不同的,对于空返回类型而言,虽然方法不会返回任何内容,但是仍然可以修改返回类型,使其存在一个返回值。但构造器不同,无法进行这种修改。

    构造器常被用于对象的初始化。但对类而言,所有的基本类型的对象引用都会被自动初始化,这一过程是先于构造器的,因此使用构造器进行初始化不是强制的。

无参构造器

        之前提到过,无参构造器,即没有参数的构造器,通常被用于创建“默认对象”。另外,即使我们没有为自定义的类创建构造器,编译器也会自动为这个类添加一个无参构造器。但是,如果我们已经创建了一个构造器,那么编译器就不会再自动进行无参构造器的创建了。例如:

class Tmp {
    Tmp(int i) {
    }

    Tmp(double d) {
    }
}

public class NoSynthesis {
    public static void main(String[] args) {
        // Tmp t = new Tmp();
        Tmp t1 = new Tmp(1);
        Tmp t2 = new Tmp(1.0);
    }
}

        若在上述程序中,执行被注释的语句Tmp t = new Tmp();的话,编译器会提示找不到匹配的构造器,并进行报错:

方法的重载

        在编程语言中,命名是一个很重要的特性,比如方法就是对动作的命名。重载的含义在于,同一个词可以表达多种不同的含义,这更加接近我们使用语言的习惯。

        Java之所以需要使用重载,有两个原因:

  1. 一个原因是因为不是每一个元素都需要一个唯一的标识符(即使省略一些不必要的词汇,语义依旧是连贯的)
  2. 而另一个原因,就是因为构造器。在编程中,我们可能会需要通过不同的方式进行对象的创建,这就需要多个构造器。但无论有几个构造器,它们都只会有一个相同的名字:类名。为此,就需要使用方法重载来区分不同的构造器。

        构造器和方法的重载用法都较为简单:

class Tree {
    int height;

    Tree() {
        System.out.println("这是一颗树苗");
        height = 0;
    }

    Tree(int init) {
        height = init;
        System.out.println("创建一棵高度为" + height + "米的树");
    }

    void info() {
        System.out.println("树的高度是" + height + "米");
    }

    void info(String s) {
        System.out.println(s + ":树的高度是" + height + "米");
    }
}

public class Overloading {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            Tree t = new Tree(i);
            t.info(); // 调用重载的方法
            t.info("系统提示");
            System.out.println("--------------------------");
        }
        System.out.println(); // 调用重载的构造器
        new Tree();
    }
}

        上述程序执行的结果是:

区分重载

        重载中,不同的方法有相同的名字,为了区分它们,规定:每一个重载方法都必须有独一无二的参数类型列表(实际上,就算只是参数顺序的不同也可以区分方法,但是这样产生的代码通常更难维护。)

     通过返回值无法区分重载。这是因为有时我们并不关心返回值,而仅仅只是调用了该方法,此时Java可能无法区分有返回值的方法和无返回值的方法。因此,不使用返回值区分重载。

使用基本类型的重载

        已知,基本类型可以从较小的类型自动提升到较大的类型。这一特性有时也会发生重载中:

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

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

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

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

    void testint(){
        int x = 0;
        System.out.print("int: ");
        f1(x);
        f2(x);
        System.out.println();
    }

    public static void main(String[] args) {
        PrimitiveOverloading p = new PrimitiveOverloading();
        p.testChar();
        p.testint();
    }
}

        程序运行的结果如下:

        注意,方法f2()的重载中并没有关于char类型的参数列表。因此charf2()中并没有被精确匹配,而是被提升为了int类型。除此之外,若传入的数据类型比重载方法的参数类型大,就必须使用窄化转型,否则编译器会发生报错。

this关键字

        当多个同一类型的对象分别调用同一个方法时,编译器会进行一些幕后工作,以此保证被调用的方法能够知道自己是被谁调用。这就是隐藏参数。例如:

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

public class BananaPeel {
    public static void main(String[] args) {
        Banana a = new Banana();
        Banana b = new Banana();
        a.peel(1);  //a、b调用了同一个方法
        b.peel(2);
    }
}

        在上述的程序中,方法peel()有一个隐藏参数,位于所以参数之前,代表着被操作对象的引用。所以,上述a和b调用方法的语句也可以写成:

Banana.peel(a, 1);
Banana.peel(b, 2);

    当然,我们不能通过这种形式进行程序的编写。

        上述这个被隐藏的引用是无法通过一般方式进行使用的,因为它根本不在参数列表中。这时就需要使用到一个Java提供的关键字:this(该关键字只能在非静态方法中使用)。这一关键字就代表了对象的引用,可以像使用任何其他对象一样使用this

        另外,若在类的一个方法内部调用该类的另一个方法,并不需要使用this

public class Apricot {
    void pick() {
        // ...
    }

    void pit() {
        pick(); // 此处可以直接调用pick()
        // ...
    }
}

        上述代码的语句pick();原本应该是this.pick();,但这样写是不必要的。因为编译器会自动进行补充。需要用到this的场景一般是:当必须明确指出当前对象的引用时。例如:

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();
    }
}

        上述程序的输出结果是:i = 3。因为increment()方法会返回当前对象的引用,因此可以轻易执行对同一对象的若干操作。

        this本身也可以被作为参数传递到其他方法中:

class Person {
    public void eat(Apple apple) {
        Apple peeled = apple.getPeeled();
        System.out.println(("有人吃了一个苹果"));
    }
}

class Peeler {
    static Apple peel(Apple apple) {
        return apple;
    }
}

class Apple {
    Apple getPeeled() {
        return Peeler.peel(this);
    }
}

public class PassingThis {
    public static void main(String[] args) {
        new Person().eat(new Apple());
    }
}

        程序执行后,会显示一行输出:有人吃了一个苹果。在上述的代码中,类Apple调用了外部的方法Peeler.peel(),此时为了将自身传递出去,就需要使用this

在构造器中调用构造器

        若一个类有多个构造器,为了避免代码的重复,就需要从一个构造器调用另一个构造器。这种情况就可以使用this关键字进行调用。

        在构造器中,this关键字可以有两个含义:

  1. 即通常的含义,表示对当前对象的引用;
  2. 若在this后加上参数列表,此时的this会显式调用与参数列表相匹配的构造器。

        下面是一个调用其他构造器的例子:

public class Invoke {
    int count = 0;
    String s = "初始值";

    Invoke(int x) {
        count = x;
        System.out.println("该构造器只有一个int类型的参数,count = " + count);
    }

    Invoke(String ss) {
        System.out.println("该构造器只有一个String类型的参数,初始值s = " + s);
    }

    Invoke(String s, int x) {
        this(x);
        // this(s); // 不能同时调用两个构造器
        this.s = s;
        System.out.println("该构造器有String类型和int类型的参数");
    }

    Invoke() {
        this("Hello World", 100);
        System.out.println("无参构造器被调用");
    }

    void printCount() {
        // this(11); // 不能在非构造器中调用其他构造器
        System.out.println("count = " + count + ", s = " + s);
    }
    
    public static void main(String[] args) {
        Invoke i = new Invoke();
        i.printCount();
    }
}

        程序执行的结果是:

        虽然可以通过this调用另一个构造器,但是不能同时调用两个。并且,构造器的调用必须出现在方法的最开始部分,否则会报错。

        在构造器语句Invoke(String s, int x)中,有一个参数s和成员s名字相同。为了防止产生歧义,可以使用this.s来表示成员数据。

        最后,编译器禁止在非构造器的普通方法中调用构造器。


static的含义

        一个带有static关键字的方法是没有this的。这种方法可以在没有创建对象的时候,通过类直接进行调用(Java中不允许使用全局方法)。一个类的静态方法可以访问其他静态方法和静态字段。

    然而,静态方法本身并不符合面向对象的思想。所以在设计静态方法之前,需要好好考虑。

 成员初始化

        Java对于变量的初始化有很严格的要求。对于方法的局部变量,若未进行初始化,就会出现报错。例如:

public class Tmp {
    public static void main(String[] args) {
        int i;
        i++;
    }
}

        若编译上述程序,会发生编译时错误:

        上述信息指出变量i未被初始化。尽管编译器可以赋予这种变量默认值,但强制程序员进行初始化很显然更加安全。另外,之前也提到过,如果类的字段是基本类型,那么这些字段都会得到一个初始值。例如:

public class InitialValues {
    boolean t;
    char c;
    byte b;
    int i;

    void printInitalValues() {
        System.out.println("数据类型        默认值");
        System.out.println("boolean         " + t);
        System.out.println("char            " + c);
        System.out.println("byte            " + b);
        System.out.println("int             " + i);

    }

    public static void main(String[] args) {
        new InitialValues().printInitalValues();
    }
}

        程序运行的结果如下:

        即使没有指定初始值,类的字段也会被自动初始化(char类型的初始值为0,但笔者的系统进行输出时未显示)。这种做法规避了未初始化变量的风险。

    若未进行初始化的字段是一个对象的引用,那么这个引用会被赋予一个特殊的初始值:null

        还可以通过方法来提供初始值,但方法的参数必须是已经初始化的:

public class MethodInit {
    int i = f();
    int j = g(i);

    int f() {
        return 1;
    }

    int g(int n) {
        return n * 2;
    }
}

        但是,Java不允许使用前向引用,这意味着Java对初始化的顺序做出了明确要求。所以如果将上述代码稍加修改:

public class MethodInit {
    int j = g(i);
    int i = f();

    int f() {
        return 1;
    }

    int g(int n) {
        return n * 2;
    }
}

就会得到一个警告:

    使用默认的初始化有时不会得到我们想要的结果。当进行手动的初始化时,我们就能得到更大的灵活性。

初始化顺序

        类中的变量定义的顺序会决定初始化的顺序。即使将定义分散到方法之间,变量定义依旧会在任何方法(包括构造器)调用之前被初始化。例如:

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

class House {
    Window w1 = new Window(1); // 在构造器之前被定义

    House() { // 构造器
        System.out.println();
        System.out.println("已进入构造器House");
        w3 = new Window(3); // w3在构造器之后被定义
    }

    Window w2 = new Window(2);

    void f() { // 方法
        System.out.println();
        System.out.println("调用方法f()");
    }

    Window w3 = new Window(3);
}

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

        上述程序执行的结果如下:

        在上述程序中,对Window对象的定义被分散到了方法之间,但显然它们在所以方法被调用之前就已经被初始化了。其中,对象w3被初始化了两次,一次在构造器被调用之前,一次在构造器中。一般建议在使用对象时再进行一次初始化,以保证正确的初始化。


静态数据的初始化

        对于类而言,无论创建了多少个该类的对象,这个类中的静态数据都只会占用一份存储空间。static关键字只能对字段使用,而不能用于局部变量。并且静态的字段也会被初始化。例如:

class Bowl {
    Bowl(int market) {
        System.out.println("Bowl(" + market + ")");
    }

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

class Table {
    Bowl bowl1 = new Bowl(1); // 这是一个非静态的变量

    Table() {
        System.out.println("调用构造器Table()");
        bowl2.f1(1);
    }

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

    static Bowl bowl2 = new Bowl(2);
}

public class StaticInitialization {
    public static void main(String[] args) {
        table.f2(1);
    }
    static Table table = new Table();
}

        上述程序执行的结果如下:

        static初始化仅在必要的时候发生。在上述例子中,若不创建Table对象,并且不引用Table.bowl1Table.bow2,那么Bowl类型的bowl1bowl2将永不被创建。

    若一个类存在静态成员,那么当第一次创建该类的对象,或者第一次访问该类的静态成员时,该类的所有静态数据将被初始化。(实际上,构造器也是一个静态方法。)

||| 初始化的顺序:静态字段 → 非静态字段。


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

        静态子句,又称静态块。在一个类中,可以将多个静态初始化语句放在一个“静态子句”中,例如:

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

        这段由static开头的语句块,与其他的静态初始化语句有一样的功能只执行一次,即使之前没有创建过该类的对象。例如:

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

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

class Days {
    static Day day1;
    static Day day2;
    static {
        day1 = new Day(1);
        day2 = new Day(2);
    }

    Days() {
        System.out.println("Days()");
    }
}

public class ExplicitStatic {
    public static void main(String[] args) {
        System.out.println("进入main()");
        Days.day1.f(99); // [1]
    }
    // static Days days1 = new Days(); // [2]
    // static Days days2 = new Days(); // [2]
}

        程序运行的结果是:

        其中,无论是[1]还是[2],只要有其中一段语句存在,Cups的静态初始化就会发生。

非静态实例的初始化

        对于对象的非静态变量,Java提供了一种名为实例初始化的类似语法:

class Mug {
    Mug(int maker) {
        System.out.println("Mug(" + maker + ")");
    }
}

public class Mugs {
    Mug mug1;
    Mug mug2;
    { // 实例初始化
        mug1 = new Mug(1);
        mug2 = new Mug(2);
        System.out.println("mug1 和 mug2 完成初始化");
    }

    Mugs() {
        System.out.println("Mugs()");
    }

    Mugs(int i) {
        System.out.println("Mugs(int)");
    }

    public static void main(String[] args) {
        System.out.println("进入main()方法");
        System.out.println("-----------------");
        new Mugs();
        System.out.println("完成Mugs()操作");

        System.out.println();
        new Mugs(1);
        System.out.println("完成Mugs(1)操作");
    }
}

        程序执行的结果是:

    该语法可用①于支持匿名内部类的初始化,以及②用于确保无论调用哪个显式的构造器,一些操作都会发生。

数组初始化

        Java可以通过两种语法定义数组:

int[] a1;
int a1[]; // C和C++的语法,但是Java也可以用

        在Java中,编译器不被允许指定数组的大小。当我们通过上述的方式使用数组时,我们还仅仅是拥有了这个数组的引用。但数组对象本身的空间却没有被分配。因此,就需要进行初始化。数组可以在代码的任何地方被初始化。而在创建数组时进行初始化有特殊的语法,例如:

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

        另外,由于a1是一个数组的引用,因此也存在下方的用法:

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

        在Java中,所有的数组都有一个固定的成员length,它可以查询数组内的元素个数。同时,Java中的数组也是从元素0开始计数,因此数组索引的最大下标值为length - 1

    但是,Java的数组是不允许越界访问的。

动态数组的创建

        可以使用new关键字创建数组中的元素,例如:

import java.util.Arrays;
import java.util.Random;

public class ArrayClassObj {
    public static void main(String[] args) {
        Random rand = new Random(47);
        Integer[] a = new Integer[rand.nextInt(20)];

        System.out.println("数组a的长度为" + a.length);
        for (int i = 0; i < a.length; i++)
            a[i] = rand.nextInt(500); // 自动装箱
        System.out.println(Arrays.toString(a));
    }
}

        程序运行的结果是:

        当使用语句Integer[] a = new Integer[rand.nextInt(20)];之后,我们可以得到一个元素是引用的数组。此时这些引用都没有关联上任何元素。直到通过后面的自动装箱为每一个引用进行初始化后,才真正完成这个数组的初始化。

    若未创建对象,而试图直接使用数组中的引用,程序就会出错。

        通过花括号,还可以通过下述语法初始化对象数组:

Integer[] a = { 1, 2, 3, }; //最后的逗号是可选的
Integer[] b = new Integer[] { 1, 2, 3, };

        通过上述对数组b初始化的方式,还可以将一个String类型的数组传递给另一个类的main()方法:

public class DynamicArray {
    public static void main(String[] args) {
        Other.main(new String[] { "你", "好", "呀" });
    }
}

class Other {
    public static void main(String[] args) {
        for (String s : args)
            System.out.print(s + " ");
        System.out.println();
    }
}

        程序执行的结果如下:


可变参数列表

        Java的可变参数列表包括:数量可变的参数和未知类型的参数。一种使用方式是通过Java的公共根类Object,可以创建一个接受Object数组的方法,例如:

import java.rmi.server.ObjID;

class A {
}

public class VarArgs {
    static void printArray(Object[] args) {
        for (Object obj : args)
            System.out.print(obj + " ");
        System.out.println();
    }

    public static void main(String[] args) {
        printArray(new Object[] { 12, (float) 3.14, 1, 11 });
        printArray(new Object[] { "Hello", "World" });
        printArray(new Object[] { new A(), new A(), new A() });
        printArray((Object[]) new Integer[] { 1, 2, 3, 4 }); // 通过强制类型转换,将数组作为列表传输
    }
}

        程序执行的结果如下:

        在第三行输出中,可以发现打印出来的类名都是@符号后面跟着十六进制数字。这就是在没有处理情况下,print()的默认行为:打印类名和对象的地址。

        除了上述的可变参数列表的表达方式外,还可以使用省略号定义一个可变参数列表:

static void printArray(Object... args) { // ...

        当然,无论通过哪种方式,最终得到的args都会是一个数组。

     但是,若是想要传递空参数列表(不传递参数,类似于printArray();)时,需要使用带有省略号的语法,即第二个语法。

        下面是一个使用省略号的可变参数列表的例子,这种参数列表在面对可选的尾随参数时很有用:

public class OptionalTrailingArguments {
    static void f(int required, String... trailing) { // 一个非Object类型的可变参数列表
        System.out.print("required: " + required + " ");
        for (String s : trailing)
            System.out.print(s + " ");
        System.out.println();
    }

    public static void main(String[] args) {
        f(1, "多出来的字符");
        f(2, "多出来了", "很多字符");
        f(3);
    }
}

        程序运行的结果如下:

        在可变参数列表中,可以使用任何类型的参数。另外,可变参数列表也可以像数组一样触发自动装箱机制,因此可以对这种参数使用for-in语句:

public class Var {
    public static void f(Integer... args) {
        for (for i : args)
            // ...
    }
}

        但是,因为这种参数列表的加入,重载过程会变得更加复杂。当有多个带有可变参数列表的类存在时,编译器可能会因此找不到目标。例如:

public class OverloadingVarargs {
    static void f(Character... args) {
        System.out.println("这个重载有一个Character类型的可变参数列表");
    }

    static void f(Integer... args) {
        System.out.println("这个重载有一个Integer类型的可变参数列表");
    }

    public static void main(String[] args) {
        f(1);
        f('a');
        f(); // 编译器不明白这到底对应着哪一个重载
    }
}

        若试图编译上述程序,会得到报错:

        为了解决上述的这种问题,可能会为某个方法添加一个非可变参数(但这样还不够)。尝试改写上面的列子:

public class OverloadingVarargs {
    static void f(float t, Character... args) {
        System.out.println("这个重载多了一个float类型的变量");
    }

    static void f(Character... args) {
        System.out.println("这个重载有一个Character类型的可变参数列表");
    }

    public static void main(String[] args) {
        f(1, 'a');
        f('a', 'b');
    }
}

        即使如此,还是无法顺利运行代码。对上述程序而言,参数列表还是存在歧义的:

         比较常见的做法是为每一个方法添加一个非可变参数:

public class OverloadingVarargs {
    static void f(float t, Character... args) {
        System.out.println("这个重载多了一个float类型的参数");
    }

    static void f(char c, Character... args) {
        System.out.println("这个重载多了一个char类型的参数");
    }

    public static void main(String[] args) {
        f(1, 'a');
        f('a', 'b');
    }
}

    不过最好的办法是在一个重载上面使用可变参数列表,或者更不不用。

清理

        Java的垃圾收集器可以回收不再被使用的对象内存,但有一些特殊情况除外:对象不使用new进行分配,而垃圾收集器只能释放由new分配的内存,此时这块空间就得不到回收。为此,Java允许在类中定义一个名为finalize的方法。

        若存在finalize()方法,那么回收内存的动作应该是:

  1. 调用finalize()方法;
  2. 下一次垃圾收集动作发生时,回收内存。

    finalize()方法并不对标C++中的析构函数。

        Java中的清理有这样的一些特点:

  • 对象可能不会被回收:当程序在运行时还没有耗尽储存空间时,一些对象存储空间可能不会被回收(因为垃圾收集本身也会有开销)
  • 垃圾收集不是析构;
  • 垃圾收集只和内存有关:任何与垃圾收集有关的动作(包括finalize()方法),都必须与内存及其的回收有关。

调用finalize()方法的时机

        一般情况下,finalize()方法是不需要被调用的,因为垃圾收集器就已经可以完成大部分的内存回收工作。但正如上面所述,若对象是以某种特殊方式被分配了存储空间,此时就会用到finalize()方法。

        之所以会发生这种情况,是因为Java允许调用本地方法,而通过本地方法又可以调用非Java代码。Java的本地代码目前只支持C和C++,但C和C++又能调用其他方法。所以,Java实际上可以调用的代码范围十分广泛。

        为了处理这种可能的由非Java代码分配的空间,就会需要使用finalize()方法进行处理。

必须执行的清理

    Java不允许创建本地对象,任何对象的创建都离不开new。并且,Java并不存在用于释放对象的操作符(像delete),这是因为垃圾收集器可以处理这一切。但除了内存释放之外,还可能存在其他的清理操作。此时就需要显式调用恰当的方法,这些方法类似于C++的析构函数。

finalize()的特殊用法

        finalize()本身并不依赖于每次都被调用。因此,可以通过这一特性对程序进行一定的检测:

class Book {
    boolean checkedOut = false;

    Book(boolean checkOut) {
        checkedOut = checkOut;
    }

    void checkIn() {
        checkedOut = false;
    }

    @SuppressWarnings("deprecation") // 用于禁止在JDK 8以上的版本在编译时通过警告信息
    @Override 
    public void finalize() {
        if (checkedOut)
            System.out.println("错误:检测失败");
        // super.finalize(); // 调用基类版本
    }
}

public class TerminationCondition {
    public static void main(String[] args) {
        Book novel = new Book(true);
        novel.checkIn(); // 通过checkIn()进行正常的清理流程
        
        new Book(true); // 引用丢失
        System.gc(); // 向JVM请求进行垃圾收集和finalize()
    }
}

        程序执行后,打印结果:错误:检测失败。通过这种方式,就可以对未正确清理的对象进行验证,以提供更多的错误信息。

        另外,在上述代码中出现了@Override。其中,@表示注解,用于提供代码的额外信息。在这里,@Override是为了告诉编译器,我们要自定义finalize()方法。并且由于finalize()方法在JDK 8之后不被推荐使用,所以需要用@SuppressWarnings("deprecation")来屏蔽警告。

    应该假设finalize()的基类版本做了某些重要的工作,因此要使用super.finalize();。由于异常处理,上述例子为使用该语句。


垃圾收集器的工作原理

        对许多编程语言而言,在堆上分配对象需要较大的代价。但对于Java而言,由于垃圾收集器的存在,其在堆上分配储存的速度,不亚于其他语言在栈上分配储存的速度

        这种看起来挺神奇的操作源于一些Java虚拟机(JVM)的工作方式。对于Java而言,在堆上分配空间只意味着让“堆指针”简单地移动到尚未分配的区域。因此它的效率可以与C++的栈分配相媲美。

        当然,如果原理仅仅只是那么简单,那么内存终究有被耗尽的时候。当“堆指针”过度远离其的起始位置时,就有可能发生缺页错误。这就需要垃圾处理器的介入了:在回收垃圾的同时,垃圾处理器会压缩堆中的空间,让“堆指针”重新往起点靠近。由此,就形成了一个高速、且空间巨大的堆模型。

    垃圾收集器会通过不同的方案提高回收垃圾收集的速度。一些有多个方案的JVM还会使用一种自适应的垃圾收集方案。

        除了垃圾收集方案以外,Java也有其他的附加技术用以提升速度。例如编译器会将程序部分或者全部编译为本地机器码。

枚举类型

        Java 5添加了enum关键字,使得Java编程能够使用枚举类型。例如:

public enum Color {
    BLUE, RED, YELLOW
}

    枚举类型的实例都是常量,因此一般使用全大写的形式。

        使用枚举的方式也很简单,只需要创建引用,并且分配其的实例即可。例如:

Color col = Color.BLUE;

        另外,Java为枚举类型添加了一些方法,例如:

  • toString()方法,可用于显示enum实例的名字,在打印枚举类型时这个方法也会被自动调用;
  • ordinal()方法,表示特定enum常量的声明顺序;
  • values()方法,按照声明顺序生成一个enum常量值的数组;
  • ......

        这些方法的使用方式可参考下方的例子:

public class EnumOrder {
    public enum Color {
        BLUE, RED, YELLOW
    }

    public static void main(String[] args) {
        for (Color col : Color.values())
            System.out.println(col + ", ordinal: " + col.ordinal());
    }
}

        程序运行的结果如下:

    同时,枚举类型也经常在switch语句中被使用。

局部变量类型判断

        在JDK 10中加入了一个新的特性:类型推断。在JDK 11中,这一用来简化局部变量定义的特性得到了改进。通过var关键字就可以启用:

class Tmp {
}

public class TypeInfrence {
    void meth() {
        String s1 = "Hello";
        var s2 = "Hello"; // 可用于显式类型

        Tmp t1 = new Tmp();
        var t2 = new Tmp(); // 也可用于用户定义的类型
    }

    static void staticMethod() { // 也可用于静态方法
        var s3 = "Hello";
        var t3 = new Tmp();
    }
}

        但是,var在使用时也存在一些限制,例如:

  1. 不能在字段上使用类型推断,例如:
    class NoInference {
        String field1 = "可以这样做";
        var field2 = "不能这样做";
    }

    其中,field2的初始化方式就是不被允许的;

  2. 必须提供初始化数据,但不能是null,例如:
    class NoInference {
        void method() {
            var noInitializer; // 初始化数据不存在,报错
            var aNull = null; // 初始化数据为null,报错
        }
    }
  3. 不能用于方法的参数,例如:
    class NoInference {
        void method(var no) { //参数为空,报错
            // ...
        }
    }

        类型推断多用于for循环,下面是一个正确使用类型推断的例子:

public class ForTypeInference {
    public enum Color {
        BLUE, RED, YELLOW
    }

    public static void main(String[] args) {
        for (var col : Color.values())
            System.out.println(col);
    }
}

    Java由于向后兼容的关系,类型推断受到限制。在使用的时候,可以先进行尝试,然后让编译器来提示是否可行。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值