用构造器确保初始化
初始化和清理是涉及安全的两个问题
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对象,并把对象赋给引用,初始化进程才算结束。