Java编程思想---第五章 初始化与清理(上)

第五章 初始化与清理(上)

  随着计算机革命的发展,不安全的编程方式已逐渐成为编程代价高昂的主因之一。初始化和清理正是设计安全的两个问题。许多程序的错误都源于程序员忘记初始化变量,特别是在使用程序库时。当使用完一个元素时,很容易把对你不再有影响的元素忘记,这样一来这个元素占用的资源就会一直得不到释放,结果是资源的耗尽,特别是内存资源。

  Java采用了构造器(constructor),并额外提供了垃圾回收器,对于不再使用的内存资源,垃圾回收器能自动将其释放。

 

5.1 用构造器确保初始化

 

  可以假象为编写的每个类都定义一个initialize()方法,在你使用其对象之前应该首先调用initialize()方法。这同时也意味着用户必须记得自己去调用这个方法。在Java中,通过停供构造器,类的设计者可以确保每个对象都会得到初始化。创建对象时,如果其类具有构造器,Java就会在用有能力操作对象之前自动调用相应的构造器,从而保证初始化的进行。在Java中采用了和C++类似的命名方法,也就是构造器与类采用相同的名称。

  以下是一个带有构造器的简单类:

class Rock {
    Rock(){
        System.out.print("Rock ");
    }
}

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

  输出结果如下:

 

  现在创建对象时将会为对象分配存储空间,并调用相应的构造器,这就确保你在操作对象之前它就已经被初始化了。

  不接受任何参数的构造器叫做默认构造器,Java文档中通常使用术语无参构造器,但和其他方法一样,构造器也能带有形式参数以便制定如何创建对象,对上面的例子稍加修改:

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

public class test {
    public static void main(String[] args) {
        for (int i = 0; i < 8; i++){
            new Rock2(i);
        }
    }
}

  输出结果如下:

  有了构造器形式参数,就可以在初始化对象时提供实际参数。如果上述例子中,Rock2(int)Rock类中唯一的构造器,那么编译器将不会允许你以任何其他方式创建Rock对象。

  构造器有助于减少错误,使代码更易于阅读。在Java中,初始化和创建两者捆绑在一起,不能分离。构造器是一种特殊类型的方法,它没有返回值,这不同于voidvoid是空返回值。

 

5.2 方法重载

 

  任何程序设计语言都具备的一项重要特性就是对名字的运用,当创建一个对象时,也就给这个对象分配到的存储空间取了一个名字,通过使用名字,你可以引用所有的对象和方法。

  在Java中,构造器是强制重载方法名的另一个原因,既然构造器的名字已经由类名决定,就只能有一个构造器名,如果想要用多种方式创建一个对象,我们就需要有两个甚至多个构造器。由于都是构造器,他们必须有相同的名字,为了让方法名相同而形式参数不同的构造器同时存在,必须用到方法重载。

  示例使用重载的构造器和重载方法:

  

class Tree {
    int height;
    Tree(){
        System.out.println("Planting a seeding");
        height = 0;
    }

    Tree(int initialHeight){
        height = initialHeight;
        System.out.println("Creating new Tree that is " + height + " feet tail");
    }

    void info(){
        System.out.println("Tree is " + height + " feet tail");
    }

    void info(String s){
        System.out.println(s + ": Tree is " + height + " feet tail");
    }
}

public class test {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++){
            Tree t = new Tree(i);
            t.info();
            t.info("overloaded method");
        }
        new Tree();
    }
}

  输出结果如下:

Creating new Tree that is 0 feet tail

Tree is 0 feet tail

overloaded method: Tree is 0 feet tail

Creating new Tree that is 1 feet tail

Tree is 1 feet tail

overloaded method: Tree is 1 feet tail

Creating new Tree that is 2 feet tail

Tree is 2 feet tail

overloaded method: Tree is 2 feet tail

Creating new Tree that is 3 feet tail

Tree is 3 feet tail

overloaded method: Tree is 3 feet tail

Creating new Tree that is 4 feet tail

Tree is 4 feet tail

overloaded method: Tree is 4 feet tail

Planting a seeding

 

5.2.1 区分重载方法

 

  如果几个方法有相同的名字,Java该如何进行区分?规则就是,每个重载的方法都必须有独一无二的参数列表。例如void f(String s, int i)void f(int i, String s)就是两个不同的方法。

 

  5.2.2 设计基本类型的重载

 

    如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型就会被提升。char型有所不同,如果无法找到恰好接收char参数的方法,就会把char直接提升至int

    如果传入的实际参数大于重载方法声明的形式参数,就得通过类型转换来执行窄化转换,不这样做的话编译器就会报错。

 

  5.2.3 以返回值区分重载方法

 

    例如这两个方法:void f() {} int f() {},这两个方法虽然有相同的名字和形式参数,但是很容易就能区分它们。

 

5.3 默认构造器

 

  默认构造器又称无参构造器,是没有形式参数的,它的作用是创建一个默认对象,如果你的类中没有构造器,编译器会自动帮你创建一个默认构造器。例如:

class Bird {}

public class DefaultConstructor {
  public static void main(String[] args) {
    Bird b = new Bird();
  }
}

  表达式new Bird()创建了一个新对象,并调用其默认构造器,没有这个默认构造器的话就无法创建对象。但如果已经定义了一个构造器,编译器就不会帮你自动创建构造器。

 

5.4 this关键字

 

  this关键词只能在方法内部使用,表示对“调用方法的那个对象”的引用,如果在方法内部调用同一个类的不同方法就不必使用this,直接调用即可。例如:

  

public class test() {
  void func1() { /* ... */ }
  void func2() { pick(); /* ... */ }
}

  在func2()内部,你可以写func1()但是没有这个必要,编译器会帮你自动添加,只有当需要明确指出当前对象的引用时才需要使用this关键字。

 

5.4.1 在构造器中调用构造器

 

  可能会为了一个类写多个构造器,还有时可能在一个构造器中调用另一个构造器,这时可以用this关键字做到这一点。

  通常用this时,都是指“这个对象”、“当前对象”,表示对当前对象的引用。在构造器中,如果this添加了参数列表那么就有了不同的含义,这将产生对符合此参数列表的某个构造器的明确使用,这样,调用其他构造器就有了直接的途径。

  尽管可以使用this调用一个构造器,但是不能调用两个,此外必须将构造器调用置于最起始处,否则编译器会报错。如果参数名称和数据成员名字相同会产生歧义,使用this也能解决这个问题。

 

5.4.2 static的含义

 

  static方法就是没有this的方法,在static方法内部不能调用非静态方法,反过来倒是可以的,而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法,这实际上正式static方法的主要用途,它很像全局方法,Java中是禁止全局方法的,但你在类中置入static方法就可以访问其他static方法和static域。

 

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

 

  程序员都了解初始化的重要性,但常常会忘记同样重要的清理工作,把一个对象用完后就弃之不顾的做法并非总是安全的,Java有垃圾回收器负责回收无用对象占据的内存资源,但也有特殊情况,假定你的对象(并非使用new)获得了一块特殊的内存区域,由于垃圾回收器只知道释放哪些经由new分配的内存,所以它不知道怎么释放该对象的内存。为应对这种情况,Java允许在类中定义一个名为finalize()的方法,它的工作原理是:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。

  这里的finalize()C++中的析构函数并不一样,在C++中,对象一定会被销毁,而Java里的对象却并非总是被垃圾回收,在Java中,对象可能不被垃圾回收,垃圾回收并不等于析构。Java并未提供析构函数或者相似的概念,要做类似的工作必须自己动手创建一个执行清理工作的普通方法。

 

5.5.1 finalize()的用途何在

 

  通过上述论述,我们应该知道不应该将finalize()作为通用的清理方法,那么finalize()真正用途是什么?

  首先我们要知道,垃圾回收只与内存有关,也就是说,使用垃圾回收器的唯一原因是为了回收程序不再使用的内存,所以对于与垃圾回收有关的任何行为来说,尤其是finalize()方法,它们也必须同内存及其回收有关。但这不意味着当对象中含有其他对象时,finalize()方法就要明确释放那些对象,不管对象时如何创建的,垃圾回收器都会负责释放对象占据的所有内存。这就将finalize()的需求限制到了一种特殊情况,即通过某种创建对象方式以外的方式为对象分配了存储空间。

  之所以要有finalize(),是由于在分配内存时可能采用了类似C语言中的做法,所以实际上在非Java代码中,也许会调用Cmalloc()函数系列来分配存储空间,而且除非调用了free()函数,否则存储空间将得不到释放从而造成内存泄露。

 

5.5.2 你必须实施清理

 

  要清理一个对象,用户必须在需要清理的时刻调用执行清理的方法。Java不允许创建局部对象,必须使用new创建对象。在Java中,可以肤浅地认为正是由于垃圾收集机制的存在,使得Java没有析构函数,但是随着学习的深入,我们应该知道垃圾回收器的存在并不能完全代替析构函数。如果希望进行除释放存储空间之外的清理工作,还是得明确调用某个恰当的Java方法。

 

5.5.3 终结条件

 

  通常,Java不能指望finalize(),必须创建其他的清理方法,并且明确调用他们。finalize()还有一个有趣的用法,它并不依赖于每次都要对finalize()进行调用,这就是对象终结条件的验证。

  当对某个对象不再感兴趣,也就是它可以被清理时,这个对象应该处于某种状态,使它占用的内存可以安全地被释放。只要对象中存在没有被适当清理的部分,程序就存在很隐晦的缺陷,finalize()可以用来最终发现这种情况,尽管它并不是总会被调用。

 

5.5.4 垃圾回收器如何工作

 

  有一种做法名叫停止-复制(stop-and-copy),显然这意味着,先暂停程序的运行,然后将所有的存活对象从当前堆复制到另一个堆,没有被复制的全是垃圾。当对象被复制到新堆时,他们是一个挨着一个的,所以新堆保持紧凑队列,然后就可以直接分配新空间了。

  但是对于这种所谓的复制式回收器而言,效率会降低。首先,得有两个堆,然后得在这两个分离的堆之间来回倒腾,从而得维护比实际需要多一倍的空间。第二个问题在于复制,程序进入稳定状态之后可能只会产生少量垃圾,甚至没有垃圾,尽管如此,复制式回收器仍然会将所有内存从一处复制到另一处,这很浪费。为了避免这种情形,一些Java虚拟机会进行检查:要是没有垃圾产生,就会转换到另一种工作模式(自适应),这种模式成为标记-清扫(mark-and-sweep)。对于一般用途而言,这种方式线速度相当慢,但是当你知道只会有少量垃圾甚至没有垃圾时它的速度就很快了。

  标记-清扫所依据的思路同样是从堆栈和静态存储区出发,遍历所有的引用,进而找出所有存活的对象,每找到一个就给对象设一个标记,这个过程不回收任何对象,只有当全部的标记工作完成的时候才会开始清理。在清理过程中,没有标记的对象将被释放,所以剩下的堆空间是不连续的,要是想得到连续空间的话就得重新整理剩下的对象。

 

5.6 成员初始化

 

  Java尽力保证所有在使用前都能得到恰当的初始化,对于方法的局部变量,Java以编译错误的形式来贯彻这种保证。所以如果写成:

  

void f() {
  int i;
  i++;
}

  就会得到一条出错的消息,告诉你i尚未初始化。强制程序员提供一个初始值,往往能够帮助找出程序里的缺陷。

 

5.6.1 指定初始化

 

  如果想为某个变量赋初值,最直接的办法就是在定义类成员变量的地方为其赋值。

  

public class Init {
  int i = 999;
  boolean bool = true;
  float f = 3.14f;
  //...
}

  也可以采用同样的方法初始化非基本类型的对象,如果Depth是一个类,那么可以像下面这样创建一个对象并初始化它:

public class Measurement {
  Depth d = new Depth();
  //...
}

  

  如果没有为d指定初始值就尝试使用它就会出现运行错误,告诉你一个异常。

  还可以通过调用某个方法来提供初值:

public class MethodInit {
  int i = f();
  int f() {
    return 11;
  }
}
原文地址:https://www.cnblogs.com/parable/p/11429509.html