第五章 - 初始化与清理

用构造器确保初始化

    初始化和清理是涉及安全的两个问题

    1 用户不知道为何初始化库的构件(或者是用户必须进行初始化的其他东西)。

    2 清理也是一个特殊问题,当使用完一个元素的时候,它对你也就没有影响了,所以很容易忘记,这样一来,这个元素会一直占据资源,结果就是资源(尤其是内存)会用尽。

    所以引入了“构造器”概念,在创建对象时候被自动调用的特殊方法,并额外提供了“垃圾回收器”,对于不再使用的内存资源,垃圾回收器自动将其释放。

    构造器采用与类名相同的名称。下面就是一个构造器的简单类

    

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


    

在创建对象 new  Rock();时候,会为对象分配存储空间,并调用相应的构造器。这样就确保在操作对象之前,它已经恰当的初始化了。不接受任何参数的构造器叫做默认构造器。但是和其他方法一样构造器也接受参数,以便指定如何创建对象,对上述例子稍加修改,即可使构造器接受一个参数

    

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


    有了构造器形式参数,就可以再初始化时提供实际参数。

    如果类中有唯一的构造器,那么编译器不允许以其他任何方式创建对象。

方法重载

    构造器是强制重载,既然构造器的名字已经由类决定了,就只能有一个构造器名。那么想要用多种方式创建一个对象该怎么办呢?假设创建一个类,既可以用标准的方式初始化,也可以从文件里读取信息来初始化。这就需要两个构造器,一个默认构造器,一个带参数的构造器。由于都是构造器,所有他们必须有相同的名称,既类名。为了让方法名相同而参数不同必须用到方法重载。

    下面的例子示范了重载的构造器和重载的方法

    

 
class Tree {
  int height;
  Tree() {
    print("默认构造器");
    height = 0;
  }
  Tree(int initialHeight) {
    height = initialHeight;
    print("带参数构造器" + height);
  }    
  void info() {
    print("重载方法1");
  }
  void info(String s) {
    print("重载方法2" + s);
  }

  private void print(String s) {
    System.out.println(s);
  }
}

public class Overloading {
  public static void main(String[] args) {
      new Tree();
      Tree t = new Tree(1);
      t.info();
      t.info("overloaded method");
  }    
}


    

区分重载方法   

    要是几个方法都有相同的名字,Java如何知道你指的是哪个呢?其实很简单,每个重载的方法必须有独一无二的参数列表。 比如参数类型的差异,参数的顺序,参数的个数。但是注意的是不能用返回值来区分方法的重载。因为在调用方法的时候可能并不会关心返回值。

 默认构造器

    如前所述,默认构造器就是没有参数的构造器,如果类中没有写构造器,那么编译器会自动帮助创建一个默认构造器,但是如果已经定义了一个构造器(无论是否有参数),编译器都不会帮助自动创建默认构造器了。

this关键字

    如果同一个类型的两个对象,分别是a和b

    

 
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);
    b.peel(2);
  }
} 


    在peel()方法中,它是如何知道自己是被a还是b调用的呢?其实在编译器内部做了一些幕后工作,它暗自把“做操作对象的引用”作为第一个参数传递给了peel()。所以上述两个方法的调用就变成了

a.peel(a,1);

b.peel(b.2);

    这是内部的表现形式,但是我们不能这样写代码,并试图编译!

    假设希望在方法内部获取当前对象引用,由于引用是编译器“偷偷”传入的,所以并没有标识符。但是提供了专门的关键字“this”,this只能在方法内部使用,表示“当前对象的引用”。例如需要返回当前对象的引用是,就常常可以在return里这样写

    

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


由于increment()通过this关键字返回了当前对象的引用,所以很容易在一条语句里对同一个对象执行多次操作。

this关键字对于将的当前对象传递给其他方法也很有用;

 
class Person {
  public void eat(Apple apple) {
    Apple peeled = apple.getPeeled();
    System.out.println("Yummy");
  }
}

class Peeler {
  static Apple peel(Apple apple) {
    // ... remove peel
    return apple; // Peeled
  }
}

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

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


    在构造器中调用构造器

    可能为一个类写了多个构造方法,有时可能想在一个构造器中调用另一个构造器,以避免重复代码。可用this关键字做到这一点。

 
public class Flower {
  int petalCount = 0;
  String s = "initial value";
  Flower(int petals) {
    petalCount = petals;
    System.out.println("Constructor w/ int arg only, petalCount= "  + petalCount);
  }
  Flower(String ss) {
    System.out.println("Constructor w/ String arg only, s = " + ss);
    s = ss;
  }
  Flower(String s, int petals) {
    this(petals);
    this.s = s;
    System.out.println("String & int args");
  }
  Flower() {
    this("hi", 47);
    System.out.println("default constructor (no args)");
  }
  void printPetalCount() {
    System.out.println("petalCount = " + petalCount + " s = "+ s);
  }
  public static void main(String[] args) {
    Flower x = new Flower();
    x.printPetalCount();
  }
}


    

static的含义

       了解this关键字后,就能更全面的理解static关键字了,static方法就是没有this的方法。在static方法的内部不能调用非静态方法,反过来倒是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类名来调用static方法。这实际正是static方法的主要用途。

    成员初始化

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

    

public class Test {
    public static void main(String[] args) {
        A a = new A();
        a.f();
    }

}
class A {
    public void f (){
        int i;
        ++i;
        System.out.println(i);
    }
}

   执行此代码会得到一个错误信息,因为i可能尚未初始化。当然,编译器也可以为i赋一个默认值,但是未初始化的局部变量更有可能是程序员的疏忽,所以采用默认值反而会掩盖这种失误。

    要是类中的数据成员是基本类型,情况就会不一样,类的每一个基本类型数据成员都会有一个默认值,现在将上面代码稍微改一下

       

public class Test {
    public static void main(String[] args) {
        A a = new A();
        a.f();
    }

}
class A {
    int i;
    public void f (){
        ++i;
        System.out.println(i);
    }
}

    再次执行,就会发现上面的代码是可以正常执行的,i被赋值了默认值0。对于对象引用时,如果不进行初始化,会得到一个特殊值null。

    初始化顺序

        在类的内部,变量定义的先后顺序决定了初始化的顺序。即时变量定义散布在方法定义之间,他们仍旧会在任何方法(包括构造方法)被调用之前得到初始化

    

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

class House {
  Window w1 = new Window(1);
  House() {
    System.out.println("House()");
    w3 = new Window(33);
  }
  Window w2 = new Window(2);
  void f() {
    System.out.println("f()"); 
  }
  Window w3 = new Window(3);
}

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

静态数据的初始化

    无论创建多少个对象,静态数据都只有一份存储区域。static关键字不能应用与局部变量,因此只能作用于域。

    静态存储区域是何时初始化的,看下面的例子:
    
class Table {

  static {
    System.out.println("Table static");
  }

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

class Cupboard {
  Cupboard() {
    System.out.println("Cupboard()");
  }
}

public class StaticInitialization {
  public static void main(String[] args) {
    System.out.println("Creating new Cupboard() in main");
    new Table();//此处再次new一个对象,Table中的静态区域块中的代码是没有加载的,也就证明了静态区域块仅仅加载一次
    new Cupboard();
    table.f2(1);
  }
  static Table table = new Table();
}
    
    从上面的例子的打印结果来看,执行顺序为先静态,然后在按照顺序执行非静态代码。而且静态区域块仅仅在初始化的时候加载一次。

    数组初始化

    数组只是相同类型的,用一个表示符名称封装到一起的一个对象序列或基本类型数据序列,数组通过方括号下表操作符[]来定义和使用的。    

    要定义一个数组

    int[] a1;

    int a1[];

    两种格式的含义是一样的,但是推荐使用第一种。

    在原文中提到了一句话叫“编译器不允许指定数组的大小”,很多同学可能不理解这句话,因为我们明明可以这样定义啊 int[] a1 = new int[10];   对 ,我们是可以这样定义。 其实这句话的意思是我们不可以这样 int[10] a1这样来定义。这就又把我们带回了有关“引用”的问题上。    现在拥有的只是对一个数组的引用(已经为该引用创建了相应的存储空间),但还没给数组对象本身分配任何空间。为了给数组创建相应的存储空间,必须写初始化表达式。

    对于数组,初始化动作可以出现在代码的任何地方。也可以用一种特殊的初始化表达式,它必须在创建数组的地方出现。这种特殊的初始化是由一对花括号括起来的值组成的,在这种情况下,存储空间的分配(等价于使用new)由编译器负责。例如:

    

public class ArraysOfPrimitives {
  public static void main(String[] args) {
    int[] a1 = { 1, 2, 3, 4, 5 };
    int[] a2;
    a2 = a1;
    for(int i = 0; i < a2.length; i++)
      a2[i] = a2[i] + 1;
    for(int i = 0; i < a1.length; i++)
      System.out.println("a1[" + i + "] = " + a1[i]);
  }
}

    

    可以看到代码中给了a1的初始值,但a2却没有。本例子中a2是后面被赋给另一个数组的。由于a2和a1是相同的别名,因为通过a2所做的修改在a1中可以看到。

      数组可以在定义到时候同时进行初始化

        

public class ArrayClassObj {
  public static void main(String[] args) {
    Integer[] a = new Integer[10];
    for(int i = 0; i < a.length; i++)
      a[i] = new Random().nextInt(100); // Autoboxing
    System.out.println(Arrays.toString(a));
  }
}

    

    这里Integer[] a = new Integer[10]; 它还只是一个引用,并且直到通过创建新的Integet对象,并把对象赋给引用,初始化进程才算结束。

    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值