Thinking In Java - 2

初始化与清理

随着计算机革命的发展,“不安全”的编程方式已经逐渐成为编程代价高昂的主因之一。

  • 构造器与“垃圾回收器”
    从概念上讲“初始化”与“创建”是彼此独立的,然而在上面的代码中,你却找不到对initialize()方法的明确调用。在Java中,“初始化”和“创建”捆绑在一起,两者不能分离。
class Rock {
    Rock() {
        System.out.print("Rock ");
    }
}

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

new表达式确实返回了对新建对象的引用,但构造器本身并没有任何返回值。

  • 方法重载
    当创建一个对象时,也就给此对象分配到的存储空间取了一个名字。所谓方法则是给某个动作取的名字。通过使用名字,你可以引用所有的对象和方法。名字起得好可以使得系统更易于理解和修改。

大多数人类语言具有很强的“冗余”性,所以即使漏掉了几个词,仍然可以推断出含义。

class Tree {
    int height;
    Tree() {
        print("Planting a seedling");
    }
    Tree(int initialHeight) { //构造器重载
        height = initialHeight;
        print("Creating new Tree that is" +
            height + " feet tall");
    }
    void info() {
        print("Tree is " + height + " feet tall");
    }
    void info(String s) { //方法重载
        print(s + ": Tree is " + height + " feet tall");
    }
}

每个重载的方法都必须有一个独一无二的参数类型列表。
(甚至参数顺序的不同也足以区分两个方法。不过,一般情况下别这么做,因为这会使代码难以维护。)

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

char < byte < short < int < long < float < double

如果传入实际参数较大,就得通过类型转换来执行窄化转换function((char)x);如果不这么做,编译器就会报错。

编译器可以通过语境判断出语义,比如int x=f();中,可以根据返回值区分重载方法。不过有时并不关心返回值,你想要的是方法调用的其他效果(为了副作用而调用),这时你可能会调用方法而忽略其返回值。所以此时依靠返回值来区分重载是行不通的

  • this关键字
    为了能用简便、面向对象的语法来编写代码——即“发送消息给对象”,编译器做了一些幕后工作。它暗自把“所操作对象的引用”作为第一个参数传递给方法。

如果你希望在方法内部获得对当前对象的引用,为此有个专门的关键字this

但要注意,如果在方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可。有些人执意将this放在每一个方法调用和字段引用前,认为这样“更清楚更明确”。但是,千万别这么做。我们使用高级语言的原因之一就是它们能帮我们做一些事情。要是你把this放在一些没必要的地方,就会使读你程序的人不知所措,因为别人写的代码不会到处使用this。人们期望只在必要处使用this。遵循一种一致而直观的编程风格能节省时间和金钱。

//Simple use of the "this" keyword.

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();
} /* Output:
i = 3
*/

在构造器中调用另一个构造器,以免重复代码,可以用this关键字做到这一点。如果为this添加了参数列表,那么就有了不同的含义。这将产生对符合此函数列表的某个构造器的明确调用。除了构造器外,编译器禁止在其他任何方法调用构造器。

static方法就是没有this的方法。

可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上是static方法的主要用途。它很像全局方法。Java中禁止使用全局方法,但你在类中置入static方法就可以访问其他static方法和static域。

  • 清理:终结处理和垃圾回收
    假定你的对象(并非使用new)获得了一块“特殊”的内存区域,由于垃圾回收器只知道释放那些经由new分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存。

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

一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。所以要是你打算用finalize(),就能在垃圾回收时刻做一些重要的清理工作。

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

3.垃圾回收只与内存有关。

通过某种创建对象方式以外的方式为对象分配了内存空间时,使用finalize()这种情况主要发生在使用“本地方法”的情况下,本地方法是一种在Java中调用非Java代码的方式。在非Java代码中,也许会调用C的malloc()函数系列来分配存储空间,而且除非调用free()函数,否则存储空间将得不到释放,从而造成内存泄漏。不要过多地使用finalize()函数。

无论是“垃圾回收”还是“终结”,都不保证一定会发生。如果Java虚拟机(JVM)并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。

finalize()可以用来最终发现程序存在的很隐晦的缺陷。

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(); //Call the base-class version
    }
}

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

System.gc用于强制进行终结动作。即使不这么做,通过重复地执行程序(假设程序将分配大量的存储空间而导致垃圾回收动作的执行),最终也能找到错误的Book对象。

Java从堆分配空间的速度,可以和其他语言从堆栈上分配空间的速度相媲美。垃圾回收器对于提高对象的创建速度,却具有明显的效果。

垃圾回收器如何工作?

  • 成员初始化
    Java尽力保证:所有变量在使用前都能得到恰当的初始化。

要是类的数据成员(即字段)是基本类型,情况就会变得有些不同。尽管数据成员的初值没有给出,但它们确实有初值(char值为0,所以显示为空白)。在类里定义一个对象引用时,如果不将其初始化,此引用就会获得一个特殊值null。

public class MethodInit {
    //! int j = g(i); //Illegal forward reference
    int i = f();
    int f();
    int f() { return 11; }
    int g(int n) { return n * 10; }
}

上述程序的正确性取决于初始化的顺序,而与其编译方式无关。所以,编译器恰当地对“向前引用”发出了警告。

  • 构造器初始化
    可以用构造器来进行初始化。

要牢记:无法阻止自动初始化的进行,它将在构造器被调用之前发生。

public class Counter {
    int i;
    Counter() { i = 7; }
}

i首先会被置为0,然后变成7。对于所有基本类型和对象引用,包括在定义时已经制定初值的变量,这种情况都是成立的;因此,编译器不会强制你一定要在构造器的某个地方或在使用它们之前对元素进行初始化——因为初始化早已得到了保证。

  • 数组初始化
    编译器不允许指定数组的大小,所有数组都有一个成员length,可以通过它获知数组内包含了多少个元素(最大下标是length-1),但不能对其修改。

每次访问数组检查边界的做法在时间和代码上都是需要开销的,但是无法禁止这个功能。这意味着如果数组访问发生在一些关键节点上,它们有可能会成为导致程序效率低下的原因之一。但是基于“因特网的安全以及提高程序员生产力”的理由,Java设计者认为这种权衡是值得的。尽管你可能会受到诱惑,去编写你认为可以使得数组访问效率提高的代码,但是这一切都是在浪费时间,因为自动的编译期错误和运行时优化都可以提高数组访问的速度。

数组的创建是在运行时刻进行的。

使用花括号括起开的列表来初始化对象数组:

Integer[] a = {
    new Integer(1),
    new Integer(2),
    3,
}; // 只能用于数组被定义处
Integer[] b = new Integer[]{
    new Integer(1),
    new Integer(2),
    3,
}; // 任何地方使用
  • 枚举类型
    Spiciness的枚举类型:
public enum Spiciness {
    NOT, MILD, MEDIUM, HOT, FLAMING
}

enum在switch中的使用:

public class Burrito {
    Spiciness degree;
    public Burrito(Spiciness degree) { this.degree = degree;}
    public void describe() {
        System.out.print{"This burrito is "};
        switch(degree) {
            case NOT:       System.out.println("not spicy at all.");break;
            case MILD:      
            case MEDIUM:    System.out.println("a little hot.");break;
            case HOT:       
            case FLAMING:   
            default:        System.out.println("maybe too hot.");break;
        }
    }
    public static void main(String[] args) {
        Burrito
            plain = new Burrito(Spiciness.NOT),
            greenChile = new Burrito(Spiciness.MEDIUM),
            jalapeno = new Burrito(Spiciness.HOT);
        plain.describe();
        greenChile.describe();
        jalapeno.describe();
    }
}
  • 22
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值