《 Java 编程思想》CH05 初始化与清理

用构造器确保初始化

在 Java 中,经过提供构造器,类的设计者可确保每一个对象都会获得初始化。Java 会保证初始化的进行。构造器采用与类相同的名称java

  • 因为构造器的名称必须与类名彻底相同,因此“每一个方法首字母小写”的风格在这里不适用。
  • 构造器方法能够有参数,这样能够在初始化对象时提供实际参数。
  • 不接受任何参数的构造器叫作“默认构造器”。
  • 构造器一种特殊类型的方法,它没有返回值

方法重载

由于能够要用多种方式来建立并初始化一个对象,因此就须要多个构造器,而构造器的名称又须要和类名相同,因此必须容许方法名相同而形式参数不一样的构造器存在,因此 Java 中有方法重载算法

class Rock {
    Rock() {
        // 默认构造器
    }
    Rock(int i) {
        // 带参数的构造器
        System.out.println("i=" + i);
    }
    void print() {
        System.out.println("i = null");
    }
    void print(int i) {
        System.out.println("i = " + i);
    }
}

// 初始化
Rock r1 = new Rock(); // 调用默认构造器
Rock r2 = new Rock(1); // 调用带参数的构造器
r2.print();
r2.print(1);

区分重载方法

方法签名是由方法名和参数类型列表构成的,因此用参数类型列表区分重载方法。不能用返回值区分重载方法数组

设计基本类型的重载

因为基本类型可能会从一个“较小”的类型自动提高为一个“较大”的类型,因此在重载方法中须要特别注意:dom

  • 常数值看成int处理
  • 实参的数据类型小于形参的数据类型时,会自动提高
  • 对于 char 类型,若是找不到以 char 做为形参的方法的话,会把 char 类型提高成 int 类型
  • 若是实参大于形参的话,须要显式的强制转换不然会报编译错误

默认构造器

  • 默认构造器是一个没有形式参数的构造器,其做用是建立一个“默认对象”
  • 若是类中没有构造器,编译器会自动建立一个默认构造器
  • 若是类中已经有了一个构造器了,编译器则不会自动建立默认构造器

this 关键字

因为同一类型的对象均可以调用相同的方法,为了在方法中区分不一样的对象,会把对象的引用做为参数传递给方法,a.fun(1)在编译器内部会被翻译成ClassName.fun(a, 1),咱们能够经过this关键字在方法中访问到对象的引用。函数

  • 在方法内部调用同个类的另外一个方法不须要使用this,直接调用便可。
  • this 能够在方法内部做为参数传递给另外一个方法,也能够做为返回值(能够构造出链式操做)

在构造器中调用构造器

  • 能够利用 this 来实如今构造器中调用构造器,这样能够避免重复代码。
  • this 在一个构造器中只能调用一次构造器
  • 必须将构造器置于最开始处,不然编译器会报错
public class Flower {
    int petalCount = 0;
    String s = "initial value";

    Flower(int petals) {
        petalCount = petals;
        System.out.println("int arg ,petalCount = " + petalCount);
    }

    Flower(String ss) {
        s = ss;
        System.out.println("string arg ,s = " + s);
    }

    Flower(String s, int petals) {
        this(petals);
        // this 只能调用一次构造器
        this.s = s;
        System.out.println("string & int arg, s = " + s + ", petalCount = " + petalCount);
    }

    Flower() {
        this("hello", 24);
    }

    public static void main(String[] args) {
        Flower flower = new Flower();
        System.out.println("flower.petalCount = " + flower.petalCount);
        System.out.println("flower.s = " + flower.s);
    }
}
// int arg ,petalCount = 24
// string & int arg, s = hello, petalCount = 24
// flower.petalCount = 24
// flower.s = hello

static 的含义

static 方法就是没有 this 的方法,在 static 中不能调用非静态方法,可是反过来能够。ui

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

  1. 对象可能不会被垃圾回收
  2. 垃圾回收不等于“析构”this

    1. Java 未提供“析构函数”或类似的概念,要作相似的清理工做,必须手动建立一下执行清理工做的普通方法
  3. 垃圾回收只与内存有关lua

    1. 对与垃圾回收有关的任何行为来讲(尤为是 finalize() 方法),它们必须同内存及其回收相关
    2. finalize() 的需求一般是用于一种特殊状况,即经过某种建立对象方式之外的方式为对象分配了存储空间,这种状况主要发生在使用了“本地方法”的状况下,本地方法是一种在 Java 中调用非 Java 代码的方式。如在非 Java 代码中调用了 malloc,为了释放内存,咱们须要在 finalize() 中调用对应的本地方法进行 free。
    3. finalize() 方法的执行机制:一旦垃圾回收器准备释放对象占用的存储空间,首先调用其 finalize() 方法,而且在下一次垃圾回收动做发生时,才会真正回收对象占用的内存。

你必须实施清理

Java 中没有用于释放对象的 delete,由于垃圾回收器会自动帮你释放存储空间,所以 Java 中没有析构函数。可是垃圾回收不能彻底代替析构函数,若是但愿进行除释放存储空间以外的清理工做,咱们须要明确调用某个 Java 方法。例如某个类打开了一个文件,垃圾回收不能自动帮咱们关闭这个文件。为何这个工做不能有 finalize() 方法来完成呢,缘由其实在上面已经说明了,对象可能不会被垃圾回收,也就是说 finalize() 方法可能永远都不会被调用。翻译

若是 JVM 没有面临内存耗尽的状况,它是不会浪费时间去执行垃圾回收以恢复内存的。设计

终结条件

虽然咱们不能用 finalize() 方法来进行“清理”,可是咱们能够利用它验证某个对象的终结条件。仍是刚才那个打开文件的例子,假设在文件没有关闭的时候,垃圾回收将对象回收了,这就会产生一些很是难找的 bug。而 finalize() 能够帮助咱们发现这种 bug。

class Book {
    boolean checkedOut = false;
    Book(boolean checkOut) {
        checkedOut = checkOut;
    }
    void checkIn() {
        checkedOut = false;
    }

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

public class TerminationCondition {
    public static void main(String[] args) {
        Book novel = new Book(true);
        novel.checkIn();
        new Book(true);
        System.gc();
    }
}
// Error: checked out

如上面这个例子,咱们但愿 Book 在被回收前已经 checkIn 了,因此咱们在 finalize() 中写了一个条件语句来判断。

  • System.gc()强制 GC
  • 应该老是假设基类的 finalize() 也须要作某些时间,因此咱们应该在 finalize() 函数的末尾加入 super.finalize();

垃圾回收器如何工做

垃圾回收器会提升对象在堆上建立的速度,这是由于 Java 的堆的实现与 C++ 的不一样,其更像是一个传送带,每分配一个对象,它就往前移动一格,因此“堆指针” 只是简单的移动到还没有分配的空间,这意味 Java 中在堆上的分配速度很是快。固然,若是只是简单的像传送带同样工做的话,Java 的堆会占用大量的虚拟内存,进而致使频繁的页面调度,并可能会致使内存资源耗尽,所以须要有垃圾回收器的介入。垃圾回收会一边回收空间一边对堆进行“紧凑”操做。

几种常见的垃圾回收机制:

  • 引用计数:一种简单但比较慢的垃圾回收机制。

    • 每一个对象都有一个引用计数器,当引用链接对象时,引用计数加一,当引用离开做用域或被置 null 时,引用计数减一。
    • 这种方法没法处理“循环引用”的状况。
  • 中止-复制(stop-and-copy):

    • 其依据的思想是:对任何“活”的对象,必定能最终追溯到其存活在堆栈或静态存储区域之中。所以,能够从堆栈和静态存储区开始,遍历全部的引用,并递归查找该对象所包含的全部对象,便可找到全部“活”的对象。
    • 该机制会先暂停程序的运行,而后将全部存活的对象从当前堆复制到另外一个堆,而后更新引用
    • 当对象被复制到新的堆中时,没复制的则至关于被回收了,同时能够实现“紧凑”的目标。
    • 该机制会有如下两个缺点:

      • 须要两个堆,进而须要维护比以前大一倍的空间
      • 若是只有少许垃圾甚至没有垃圾,而这时若是进行垃圾回收的话,开销太大了。
  • 标记-清扫(mark-and-sweep):

    • 与 stop-and-copy 机制依据的思想是同样的也是,用一样的方式找到“活”的对象
    • 每当它找到一个“活”的对象,就会给该对象一个标记,这个过程当中不会回收任何对象。只有当所有标记工做完成时,才会进行清理。
    • 清理的过程当中,没有被标记的对象被释放,但不会作任何复制动做
    • 为了不存储空间的“碎片化”,JVM 须要作紧凑操做

JVM 中采用的垃圾回收机制:

  • 一种结合的 stop-and-copy 和 mark-and-sweep 的自适应垃圾回收算法
  • 内存分配以较大的“快”做为单位,较大的对象能够独占一个块。每一个块都有相应的代数(generation count)来记录它是否存活。
  • 垃圾回收会对上次回收操做以后新分配的快进行整理,这样有助于解决有大量短命对象的状况。
  • 垃圾回收机制会按期进行完整的清理——大型对象仍然不会被复制(只是其代数会增长),而那些含有大量小型对象的快会被复制并整理。
  • 当只有少许或没有垃圾产生时,则转为使用 mark-and-sweep 算法。

Java 中 JIT(Just-In-Time)技术:

这种技术能够把程序所有或部分翻译成本地机器码,而不是经过 JVM,进而提高程序的运行速度。

当须要装载某个类时(第一次建立这个类时),编译器会找到其.class 文件,而后将该类的字节码装入内存,此时有两种作法:

  • JIT 直接编译全部代码,但这个作法会有两个缺点:

    • 加载动做分散在整个程序中,累加起来要话更多时间
    • 可能会增长可执行代码的长度,进而致使页面调度
  • 惰性评估(lazy evaluation):,即 JIT 只在必要时才编译,这样不会执行的代码就不会被 JIT 所编译。

成员初始化

Java 尽力保证:全部变量在使用前都能获得适当的初始化。

局部变量没有默认初始值,若有在未初始化前使用它会报错编译错误,而类变量则有默认初始值。

指定初始化

Java 容许在定义类成员变量的时候为其赋值进行初始化。非基本类型也能够,同时可使用已经函数或已经初始化好的变量进行初始化,但要保证初始化顺序的正确。

public class InitialValues {

    boolean t = false;
    char c = 'a';
    byte b = 1;
    short s = 2;
    int i = func(s);
    long l = 4 + i;
    float f = (float)5.0; // 浮点数字面量是 double 类型的
    double d = 6.0;
    String reference = new String("hello world"); // 非基本类型也能够

    int func(short s) {
        return s*2;
    }
}

构造器初始化

  • 没法阻止自动初始化的进行,它将在构造器以前执行。
  • 类变量的定义顺序决定了初始化的顺序
  • 静态数据的默认值与类变量一致
  • 对于静态变量,Java 能够将多个初始化语句组合成一个静态块,其和静态变量初始化同样在类加载时执行。顺序与定义时的顺序相同
  • 对于非静态变量,Java 中也能够将多个初始化语句组成一个块,在实例初始化执行。
  • 对于以上两种块,既能够能够把它当成一条初始化语句来看待。
public class InitialValues {

    boolean t = false;
    char c = 'a';
    byte b = 1;
    short s = 2;
    int i = 3;
    long l = 4;
    float f;
    double d;
    String reference;

    {
        f = (float) 1.0;
        d = 2*f;
        reference = new String("hello");
        reference = reference + f + d;
    }

    static {
        System.out.println("hello");
    }
    static int a;
    static {
        System.out.println("A is " + a);
        a = 2;
    }
}

对象的建立过程:

  1. 第一次建立类或者访问其静态数据或方法,JVM 会加载其 .class 文件,此时执行全部静态初始化(按定义的顺序执行)。
  2. 当 new 该类时,首先会在堆上分配空间,由于堆在分配前被置零了,因此本类型的默认值都是 0,非基本类型的引用的默认值则是 null。
  3. 按顺序执行非静态的初始化
  4. 执行构造器

数组初始化

数组是同类型的、用一个标识符名称封装到一块儿的一个对象序列或基本类型数据序列。

定义方式:

int[] a; //建议使用这种
int a[]; //这样也能够, 可是不能指定数组的类型。`int a[3];` 这样是不容许的

int[] a;这样只是定义了一个数组的引用,咱们可使用new来建立一个数组,也能够直接初始化数组:

int[] a = new int[3]; // 使用 new 来建立一个数组,这时真实数据会分配在堆中,因此默认值都为“零”
int[] b = {1, 2, 3}; // 直接初始化一个长度为3的数组
Integer[] c = new Integer[3]; // 建立一个对象数组,保存引用,这时初始值都为 null
Random rand = new Random(2);
int len = rand.nextInt(20);
int[] c = new int[len]; // 长度不必定要是一个字面值,能够是变量

数组初始化的坑点:

InitialValues initialValues = new InitialValues();
initialValues.printInitialValues();
String[] stringArray = {"hello", "world"};
// initialValues.printStringArrary({"hello", "world"}); // 编译错误
initialValues.printStringArrary(stringArray);
initialValues.printStringArrary(new String[]{"hello", "world"}); //正确打开方式
int[] intArray = {1, 2, 3, 4};
// initialValues.printIntArray({1, 2, 3, 4}); // 编译错误
initialValues.printIntArray(intArray);
initialValues.printIntArray(new int[]{1, 2, 3, 4});

可变参数列表

在方法中,用ClassName... ArgName的形式能够定义可变参数列表,在方法中,ArgName 本质上是一个数组。在可变列表中可使用任何类型,包括基础类型。这里传入基本类型时,没有依赖自动装包和解包,这意味着,ClassName 为 int 时,ArgName 是一个 int[],而不是 Integer。在重载方法时,应该只在一个方法中使用可变参数列表

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

static void f(int required, int... args) {
    System.out.println("Required: " + required);
    for(int i: args) {
        System.out.print(i + " ");
    }
    System.out.println();
}

public static void main(String[] args) {
    printArray(1, 2, 3, 4, 5);
    f(1);
    f(1, 2, 3);
    Integ
}

枚举类型

  • 按照命名习惯,枚举值通常用全大写字母
  • 为了使用 enum,须要建立一个该类型的引用
  • enum 会自动建立一些实用的函数,如toString()显示其名称,ordinal()表示声明顺序
  • enum 适合与 switch 一块儿使用
enum EnumDemo {
    HELLO, WORLD, 
};

EnumDemo e1 = EnumDemo.HELLO;
System.out.println(e1); // 自动调用toString()
System.out.println(e1.ordinal());

for(EnumDemo e: EnumDemo.values()) {
    System.out.println("EnumDemo: " + e + " ordinal " + e.ordinal());
}
// HELLO
// 0
// EnumDemo: HELLO ordinal 0
// EnumDemo: WORLD ordinal 1