[再学编程思想]第五章 初始化与清理

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

    初始化和清理 正是涉及安全的两个问题,许多c语言的错误都源于程序员忘记初始化变量,特别是在使用程序库时,如果用户不知道如何初始化库构件,更是如此,清理也是一个特殊问题,当使用完一个元素时,他对你也就不会有什么影响了,所以很容易把他忘记,这样一来这个元素就会一直得不到释放,结果是资源用尽。

    C++引入了构造器的概念,这是一个在创建对象时被自动调用的特殊方法,java也采取了构造器,并额外提供了“垃圾回收机制”对于不在使用的内存资源,垃圾回收器能自动将其释放。

一.用构造器确保初始化

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

    java中和c++采用相同的方式,即类的构造器名和类名相同。

class Rock {
  Rock() { // This is the constructor
    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 Rock() 将会为对象分配存储空间,并且调用相应的构造器。这样就确保了在能操作对象之前, 它已经被恰当的初始化了。

    需要注意的是,由于构造器的名称必须和类名完全相同,所以“每个方法首字母小写”的编码风格并不适用于构造器。

    不接受任何参数的构造器叫做默认构造器,java文档中通常使用术语无参构造器。

    构造器中也可以传入参数:

//: initialization/SimpleConstructor2.java
// Constructors can have arguments.

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

public class SimpleConstructor2 {
  public static void main(String[] args) {
    for(int i = 0; i < 8; i++)
      new Rock2(i);
  }
} /* Output:
Rock 0 Rock 1 Rock 2 Rock 3 Rock 4 Rock 5 Rock 6 Rock 7
*///:~

    有了构造器形式参数,就可以在初始化对象时提供实际参数,例如:假设类Tree有一个构造器,它接受一个整型变量来表示树的高度,就可以这样创建一个对象:Tree tree = new Tree(12)

    构造器有助于减少错误,并使代码更容易阅读,从概念上来讲,“初始化”与“创建”是彼此独立的,在java中初始化和创建捆绑在一起,两者不可分离。

    构造器是一种特殊类型的方法,因为没有返回值,这与返回值为void明显不同,对于空返回值,尽管方法本身不会自动返回什么,但仍可选择让它返回别的东西,构造器则不会返回任何东西。

二.方法重载

    方法重载是指在一个类中定义多个同名的方法,但要求每个方法具有不同的参数的类型或参数的个数。调用重载方法时,Java编译器能通过检查调用的方法的参数类型和个数选择一个恰当的方法。方法重载通常用于创建完成一组任务相似但参数的类型或参数的个数不同的方法。

import static net.mindview.util.Print.*;

class Tree {
  int height;
  Tree() {
    print("Planting a seedling");
    height = 0;
  }
  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");
  }
}

public class Overloading {
  public static void main(String[] args) {
    for(int i = 0; i < 5; i++) {
      Tree t = new Tree(i);
      t.info();
      t.info("overloaded method");
    }
    // Overloaded constructor:
    new Tree();
  }	
} /* Output:
Creating new Tree that is 0 feet tall
Tree is 0 feet tall
overloaded method: Tree is 0 feet tall
Creating new Tree that is 1 feet tall
Tree is 1 feet tall
overloaded method: Tree is 1 feet tall
Creating new Tree that is 2 feet tall
Tree is 2 feet tall
overloaded method: Tree is 2 feet tall
Creating new Tree that is 3 feet tall
Tree is 3 feet tall
overloaded method: Tree is 3 feet tall
Creating new Tree that is 4 feet tall
Tree is 4 feet tall
overloaded method: Tree is 4 feet tall
Planting a seedling
*///:~

    创建Tree对象的时候既可以不含参数,也可以用树的高度作为参数。

    区分重载方法:

    每个重载的方法都必须有一个独一无二的参数类型列表。

    涉及基本类型的重载: 

    基本类型可能会发生向上转型:即从一个“较小”的类型自动提升至一个“较大”的类型。此过程中一旦牵涉到重载,可能会造成一些混淆

//: initialization/PrimitiveOverloading.java
// Promotion of primitives and overloading.
import static net.mindview.util.Print.*;

public class PrimitiveOverloading {
  void f1(char x) { printnb("f1(char) "); }
  void f1(byte x) { printnb("f1(byte) "); }
  void f1(short x) { printnb("f1(short) "); }
  void f1(int x) { printnb("f1(int) "); }
  void f1(long x) { printnb("f1(long) "); }
  void f1(float x) { printnb("f1(float) "); }
  void f1(double x) { printnb("f1(double) "); }

  void f2(byte x) { printnb("f2(byte) "); }
  void f2(short x) { printnb("f2(short) "); }
  void f2(int x) { printnb("f2(int) "); }
  void f2(long x) { printnb("f2(long) "); }
  void f2(float x) { printnb("f2(float) "); }
  void f2(double x) { printnb("f2(double) "); }

  void f3(short x) { printnb("f3(short) "); }
  void f3(int x) { printnb("f3(int) "); }
  void f3(long x) { printnb("f3(long) "); }
  void f3(float x) { printnb("f3(float) "); }
  void f3(double x) { printnb("f3(double) "); }

  void f4(int x) { printnb("f4(int) "); }
  void f4(long x) { printnb("f4(long) "); }
  void f4(float x) { printnb("f4(float) "); }
  void f4(double x) { printnb("f4(double) "); }

  void f5(long x) { printnb("f5(long) "); }
  void f5(float x) { printnb("f5(float) "); }
  void f5(double x) { printnb("f5(double) "); }

  void f6(float x) { printnb("f6(float) "); }
  void f6(double x) { printnb("f6(double) "); }

  void f7(double x) { printnb("f7(double) "); }

  void testConstVal() {
    printnb("5: ");
    f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5); print();
  }
  void testChar() {
    char x = 'x';
    printnb("char: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testByte() {
    byte x = 0;
    printnb("byte: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testShort() {
    short x = 0;
    printnb("short: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testInt() {
    int x = 0;
    printnb("int: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testLong() {
    long x = 0;
    printnb("long: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testFloat() {
    float x = 0;
    printnb("float: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testDouble() {
    double x = 0;
    printnb("double: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  public static void main(String[] args) {
    PrimitiveOverloading p =
      new PrimitiveOverloading();
    p.testConstVal();
    p.testChar();
    p.testByte();
    p.testShort();
    p.testInt();
    p.testLong();
    p.testFloat();
    p.testDouble();
  }
} /* Output:
5: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
char: f1(char) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
byte: f1(byte) f2(byte) f3(short) f4(int) f5(long) f6(float) f7(double)
short: f1(short) f2(short) f3(short) f4(int) f5(long) f6(float) f7(double)
int: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
long: f1(long) f2(long) f3(long) f4(long) f5(long) f6(float) f7(double)
float: f1(float) f2(float) f3(float) f4(float) f5(float) f6(float) f7(double)
double: f1(double) f2(double) f3(double) f4(double) f5(double) f6(double) f7(double)
*///:~

    不能用返回值来区分重载方法。

三.默认构造器

    作用是创建一个默认对象,如果你的类中没有写构造器,编译器会帮助你创建一个默认构造器。

    如果已经定义了构造器,不管有没有参数,编译器就不会自动创建默认构造器。

四.this关键字

    如果有通一个类型的两个对象,分别是a和b你可能想知道如何才能让这两个对象都调用peel()方法呢:

   

//: initialization/BananaPeel.java

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

public class BananaPeel {
  public static void main(String[] args) {
    Banana a = new Banana(),
           b = new Banana();
    a.peel(1);
    b.peel(2);
  }
} ///:~

    为了更简洁面向对象的语法来编写代码--即“发送消息给对象”编译器做了一些幕后工作,把“所操作对象的引用”作为第一个参数传递给peel()所以上述两个方法调用编程了这样:

Banna.peel(a,1)
Banna.peel(b,2)

    这是内部的表示形式,并不能这样书写代码,但这样的方式可以帮你了解实际放生的事情。

    假设希望在方法的内部获得对当前对象的引用,由于这个引用是编译器“偷偷”传入的,所以没有标识符使用,但是为此有个专门的关键字:this,this这个关键字只能在方法内部使用, 表示“调用方法的那个对象”的引用,this的用法和其他对象的引用不无不同,但要注意,如果在方法内部调用同一个类的另一个方法,就不必使用this,直接调用即可。当前方法中的this引用会自动应用于同一个类中的其他方法。

    

//: initialization/Apricot.java
public class Apricot {
  void pick() { /* ... */ }
  void pit() { pick(); /* ... */ }
} ///:~

    在pit内部你可以写this.pick()但没有必要,因为编译器会为你自动添加,只有当需明确指出当前对象的引用时,才需要使用this关键字,例如当需要返回当前对象的引用时,尝尝在return中这样写:

    

//: initialization/Leaf.java
// 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的时候都是指“这个对象”或“当前对象”,而且他本身表示对当前对象的引用,在构造器中,如果为this添加了参数列表,那么就有了不同的含义,这将产生不同的含义,这将产生对符合此参数列表的某个构造器的明确调用:

//: initialization/Flower.java
// Calling constructors with "this"
import static net.mindview.util.Print.*;

public class Flower {
  int petalCount = 0;
  String s = "initial value";
  Flower(int petals) {
    petalCount = petals;
    print("Constructor w/ int arg only, petalCount= "
      + petalCount);
  }
  Flower(String ss) {
    print("Constructor w/ String arg only, s = " + ss);
    s = ss;
  }
  Flower(String s, int petals) {
    this(petals);
//!    this(s); // Can't call two!
    this.s = s; // Another use of "this"
    print("String & int args");
  }
  Flower() {
    this("hi", 47);
    print("default constructor (no args)");
  }
  void printPetalCount() {
//! this(11); // Not inside non-constructor!
    print("petalCount = " + petalCount + " s = "+ s);
  }
  public static void main(String[] args) {
    Flower x = new Flower();
    x.printPetalCount();
  }
} /* Output:
Constructor w/ int arg only, petalCount= 47
String & int args
default constructor (no args)
petalCount = 47 s = hi
*///:~

    尽管可以用this调用一个构造器,但却不能调用两个,此外必须将构造器调用置于最开始处,否则编译器会报错。

    static含义:

    了解this关键字之后,就能更全面的理解static方法的含义,static方法就是没有this的方法,在static方法的内部不能调用非静态的方法,返回来是可以的,而且可以在没有创建任何对象的前提下仅仅通过类本身来调用static方法,这实际上正是static方法的主要用途,它很像全局方法,java中禁止使用全局方法,但你在类中置入static方法就可以访问其他static方法和static域。

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

    假如你的对象(并非使用new)获得了一块特殊的内存区域,由于垃圾回收器只知道释放那些经由new分配的内存,所以他不知道如何释放该对象这块特殊内存,为了应对这种情况,java允许在类中定义一个名为finalize()的方法,它的工作原理“假定”是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法并且在下一次垃圾回收动作发生时,才会真正回收对象占用的空间,所以要是你打算用finalize(),就能在垃圾回收时刻做一些重要的清理工作。

    需要记住的是:

    1.对象可能不被垃圾回收

    2.垃圾回收不等于“析构”

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

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

    之所以要有finalize()是由于在分配内存时可能采用了类似c语言中的做法,而非java中的通常做法,这种情况主要发生在使用“本地方法”的情况下,本地方法是一种在java中调用非java代码得方式。

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

    对某个对象不再感兴趣,也就是它可以被清理了,这个对象应该处于某种状态,使它占用的内存可以被安全的释放。

六.成员初始化

    java尽力保证所有变量在使用前都能得到恰当的初始化,对于方法的局部变量,java以编译时错误的形式来保证。

    如果是类的成员变量是基本类型,情况会变得不同,类的每个基本类型数据成员保证会有一个初始值。

public class InitialValues{
    boolean b;
    char c;
    byte by;
    int i;
    float f;
    double d;
    void printValue(){
        System.out.println(b+" " + c + " " + by + " " + i + " " + f + " " + d);
    }
    public static void main(String[] args){
        InitialValues ii = new InitialValues();
        ii.printValue();
    }
}

//false  0 0 0.0 0.0

七.构造器初始化

    可以用构造器来进行初始化,在运行时刻,可以调用方法或执行某些动作来确定初值,这位编程带来更大的灵活性,但是要知道:无法阻止自动初始化的进行 

    通过以下代码举例:

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

    这中情况下i首先会置0,然后变成7,对于所有的基本类型和对象引用,包括在定义时已经制定初值的变量,这种情况都是成立的。

    初始化顺序:

    在类的内部,变量定义的先后顺序决定了初始化的顺序,即使变量定义散布于方法定义之间,他们仍旧会在方法被调用之前得到初始化。



// When the constructor is called to create a
// Window object, you'll see a message:
class Window {
  Window(int marker) { print("Window(" + marker + ")"); }
}

class House {
  Window w1 = new Window(1); // Before constructor
  House() {
    // Show that we're in the constructor:
    print("House()");
    w3 = new Window(33); // Reinitialize w3
  }
  Window w2 = new Window(2); // After constructor
  void f() { print("f()"); }
  Window w3 = new Window(3); // At end
}

public class OrderOfInitialization {
  public static void main(String[] args) {
    House h = new House();
    h.f(); // Shows that construction is done
  }
} 

/* Output:
Window(1)
Window(2)
Window(3)
House()
Window(33)
f()
*/

    观察代码的输出,因为调用构造方法之前,类成员变量都会得到初始化,所以当编译器想要创建一个House对象之前,发现House的成员变量中存在w1w2w3,并且还没有初始化,所以先按照顺序初始化,然后执行House的构造器,接着在构造器中存在一个w3,初始化。

     静态数据初始化:    无论创建多少个对象,静态数据都只占用一份存储空间,static关键字不能应用于局部变量,因此它只能作用于域,如果一个域是静态的基本类型域,且没有对它进行初始化,那么它就会获得基本类型的标准初值,如果它是一个对象引用,那么它的默认初值是null。

    如果想在定义处进行初始化,采取的方法与非静态数据没有什么不同。

//: initialization/StaticInitialization.java
// Specifying initial values in a class definition.
import static net.mindview.util.Print.*;

class Bowl {
  Bowl(int marker) {
    print("Bowl(" + marker + ")");
  }
  void f1(int marker) {
    print("f1(" + marker + ")");
  }
}

class Table {
  static Bowl bowl1 = new Bowl(1);
  Table() {
    print("Table()");
    bowl2.f1(1);
  }
  void f2(int marker) {
    print("f2(" + marker + ")");
  }
  static Bowl bowl2 = new Bowl(2);
}

class Cupboard {
  Bowl bowl3 = new Bowl(3);
  static Bowl bowl4 = new Bowl(4);
  Cupboard() {
    print("Cupboard()");
    bowl4.f1(2);
  }
  void f3(int marker) {
    print("f3(" + marker + ")");
  }
  static Bowl bowl5 = new Bowl(5);
}

public class StaticInitialization {
  public static void main(String[] args) {
    print("Creating new Cupboard() in main");
    new Cupboard();
    print("Creating new Cupboard() in main");
    new Cupboard();
    table.f2(1);
    cupboard.f3(1);
  }
  static Table table = new Table();
  static Cupboard cupboard = new Cupboard();
} /* Output:
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)
*///:~

    在这段代码中,可见静态初始化只有在必要时刻才会进行,如果不创建Table对象,也不引用Table.b1或Table.b2那么静态的Bowl b1 和 b2 永远不会被创建,只有在第一个Table对象被创建(或者第一次访问静态数据)的时候它们才会被初始化,此后静态对象不会再次被初始化。

    初始化的顺序是现静态对象,然后是非静态对象,要执行main()方法必须先加载StaticInitialization类,然后其静态域table和cupboard会被初始化,这导致它们对应的类也被加载,并且由于它们也都包含静态的Bowl对象,因此Bowl也被加载。这样这个特殊的程序中的所有类在main函数开始之前都被加载了。

    总结下对象创建的过程,假设有个Dog类:

    1>即使没有显式的使用static关键字,构造器实际上也是静态方法,因此当首次创建类型为Dog的对象时(构造器可以看成静态方法)或者Dog类的静态方法/静态域首次被访问时,java解释器必须查找类路径,以定位到Dog.class文件

    2>然后载入Dog.class有关静态初始化的所有动作都会执行,因此静态初始化只在class对象首次加载的时候进行一次。

    3>当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间。

    4>这快存储空间会被清零,这就自动的将Dog对象中的所有基本数据类型都设置为默认值,而引用则被设置为null。

    5>执行所有出现于字段定义处的初始化动作。

    6>执行构造器。

    显式的静态初始化:

    java允许将多个静态初始化动作组织成一个特殊的“静态子句”(静态块):

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

    非静态实例初始化:

    

//: initialization/Mugs.java
// Java "Instance Initialization."
import static net.mindview.util.Print.*;

class Mug {
  Mug(int marker) {
    print("Mug(" + marker + ")");
  }
  void f(int marker) {
    print("f(" + marker + ")");
  }
}

public class Mugs {
  Mug mug1;
  Mug mug2;
  {
    mug1 = new Mug(1);
    mug2 = new Mug(2);
    print("mug1 & mug2 initialized");
  }
  Mugs() {
    print("Mugs()");
  }
  Mugs(int i) {
    print("Mugs(int)");
  }
  public static void main(String[] args) {
    print("Inside main()");
    new Mugs();
    print("new Mugs() completed");
    new Mugs(1);
    print("new Mugs(1) completed");
  }
} /* Output:
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
*///:~

    实例初始化子句:

{
    mug1 = new Mug(1);
}

    看起来与静态初始化子句一样只是少了static关键字,这种语法对于支持“匿名内部类”的初始化是必须的,但是它也使得你可以保证无论调用了哪个显式构造器,某些操作都会发生。

八.数组初始化

    数组只是相同类型的,用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。数组是通过方括号下标操作符[]定义和使用的,要定义一个数组,只需在类型名后加上一对空方括号即可。

  数组的创建可以通过以下两种方式,推荐使用第一种,因为创建的是一个数组类型。

int[] a1;
int a1[];

    所有数组都有一个固有成员,可以通过它获知数组内包含了多少个元素,但不能对其进行修改,这个成员就是length。java数组默认也是从第0个元素开始,所以能使用的最大下标数是length-1要是超出这个边界会出现运行时错误(异常)。

    可变参数列表:由于所有类都直接或间接继承于Object类,所以可以创建Object数组为参数的方法

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[]{
      new Integer(47), new Float(3.14), new Double(11.11)
    });
    printArray(new Object[]{"one", "two", "three" });
    printArray(new Object[]{new A(), new A(), new A()});
  }
} /* Output: (Sample)
47 3.14 11.11
one two three
A@1a46e30 A@3e25a5 A@19821f
*///:~

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值