"不安全"的编程是造成编程代价昂贵的罪魁祸首之一。有两个安全性问题:初始化和清理。
Java采用了构造器的概念,另外还使用了垃圾收集器(Garbage Collector, GC)去自动回收不再被使用的对象所占的资源。这一章将讨论初始化和清理的问题,以及在 Java 中对它们的支持。
以 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();
}
}
}
输出:
Rock Rock Rock Rock Rock Rock Rock Rock Rock Rock
现在,当创建一个对象时: new Rock() ,内存被分配,构造器被调用。构造器保证了对象在你使用它之前进行了正确的初始化。
在 Java 中,对象的创建与初始化是统一的概念,二者不可分割。
方法重载是必要的,它允许方法具有相同的方法名但接收的参数不同。
—PS:方法1-dadHitme (皮带) 方法2-dadHitme (竹板)
区分重载方法
每个被重载的方法必须有独一无二的参数列表。甚至可以根据参数列表中的参数顺序来区分不同的方法。
返回值的重载
经常会有人困惑,“为什么只能通过类名和参数列表,不能通过方法的返回值区分方法呢?”。例如以下两个方法,它们有相同的命名和参数,但是很容易区分:
void f(){}
int f() {return 1;}
有些情况下,编译器很容易就可以从上下文准确推断出该调用哪个方法,如 int x = f() 。但是,你可以调用一个方法且忽略返回值。这叫做调用一个函数的副作用,因为你不在乎返回值,只是想利用方法做些事。所以如果你直接调用 f() ,Java 编译器就不知道你想调用哪个方法,阅读者也不明所以。因为这个原因,所以你不能根据返回值类型区分重载的方法。
如果你创建一个类,类中没有构造器,那么编译器就会自动为你创建一个无参构造器。
class Bird {}
public class DefaultConstructor {
public static void main(String[] args) {
Bird bird = new Bird(); // 默认的
}
}
但是,一旦你显式地定义了构造器(无论有参还是无参),编译器就不会自动为你创建无参构造器。
class Bird2 {
Bird2(int i) {}
Bird2(double d) {}
}
public class NoSynthesis {
public static void main(String[] args) {
//- Bird2 b = new Bird2(); // No default
Bird2 b2 = new Bird2(1);
Bird2 b3 = new Bird2(1.0);
}
}
this 关键字只能在非静态方法内部使用。当你调用一个对象的方法时,this 生成了一个对象引用。你可以像对待其他引用一样对待这个引用。如果你在一个类的方法里调用其他该类中的方法,不要使用 this,直接调用即可,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();
}
}
通常当你说 this,意味着"这个对象"或"当前对象",它本身生成对当前对象的引用。在一个构造器中,当你给 this 一个参数列表时,它是另一层意思。它通过最直接的方式显式地调用匹配参数列表的构造器。
static 的含义
static 方法中不会存在 this。你不能在静态方法中调用非静态方法(反之可以)。静态方法是为类而创建的,不需要任何对象。事 实上,这就是静态方法的主要目的,静态方法看起来就像全局方法一样,但是 Java 中不允许全局方法,一个类中的静态方法可以被其他的静态方法和静态属性访问。
—PS:static 修饰的方法为静态方法,归属于类,和对象无关。
Java 中有垃圾回收器回收无用对象占用的内存。但现在考虑一种特殊情况:你创建的对象不是通过 new 来分配内存的,而垃圾回收器只知道如何释放用 new 创建的对象的内存,所以它不知道如何回收不是 new 分配的内存。为了处理这种情况,Java 允许在类中定义一个名为 finalize() 的方法。
—PS:知道就行,也用不到。下面是讲GC的,不说了
Java 尽量保证所有变量在使用前都能得到恰当的初始化。对于方法的局部变量,这种保证会以编译时错误的方式呈现,要是类的成员变量是基本类型,情况就会变得有些不同。正如在"万物皆对象"一章中所看到的,类的每个基本类型数据成员保证都会有一个初始值。
public class InitialValues {
boolean t;
char c;
byte b;
short s;
int i;
long l;
float f;
double d;
InitialValues reference;
void printInitialValues() {
System.out.println("Data type Initial value");
System.out.println("boolean " + t);
System.out.println("char[" + c + "]");
System.out.println("byte " + b);
System.out.println("short " + s);
System.out.println("int " + i);
System.out.println("long " + l);
System.out.println("float " + f);
System.out.println("double " + d);
System.out.println("reference " + reference);
}
public static void main(String[] args) {
new InitialValues().printInitialValues();
}
}
结果:
Data type Initial value
boolean false
char[ ]
byte 0
short 0
int 0
long 0
float 0.0
double 0.0
reference null
—PS:类的成员变量若是基本类型会有默认值,方法的局部变量必须手动初始化
指定初始化
怎么给一个变量赋初值呢?一种很直接的方法是在定义类成员变量的地方为其赋值。
public class InitialValues2 {
boolean bool = true;
char ch = 'x';
byte b = 47;
short s = 0xff;
int i = 999;
long lng = 1;
float f = 3.14f;
double d = 3.14159;
}
可以用构造器进行初始化,这种方式给了你更大的灵活性,因为你可以在运行时调用方法进行初始化。但是,这无法阻止自动初始化的进行,他会在构造器被调用之前发生。因此,如果使用如下代码:
public class Counter {
int i;
Counter() {
i = 7;
}
}
i 首先会被初始化为 0,然后变为 7。对于所有的基本类型和引用,包括在定义时已明确指定初值的变量,这种情况都是成立的。因此,编译器不会强制你一定要在构造器的某个地方或在使用它们之前初始化元素——初始化早已得到了保证。
—PS:编译器:放着,我来
初始化的顺序
在类中变量定义的顺序决定了它们初始化的顺序。即使变量定义散布在方法定义之间,它们仍会在任何方法(包括构造器)被调用之前得到初始化。
class Window {
Window(int marker) {
System.out.println("Window(" + marker + ")");
}
}
class House {
Window w1 = new Window(1); // Before constructor
House() {
// Show that we're in the constructor:
System.out.println("House()");
w3 = new Window(33); // Reinitialize w3
}
Window w2 = new Window(2); // After constructor
void f() {
System.out.println("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
}
}
输出:
Window(1)
Window(2)
Window(3)
House()
Window(33)
f()
—PS:Window 类是 House 类中的变量,它的初始化会在任意方法之前,不论它在什么位置。所以先输出的是
Window(1)
Window(2)
Window(3)
;
接着 House 类的构造函数被调用,先输出了 House() ,紧接着 w3 再次被赋值,输出 Window(33) ;
最后调用 f() ,输出 f()
静态数据的初始化
无论创建多少个对象,静态数据都只占用一份存储区域。static 关键字不能应用于局部变量,所以只能作用于属性(字段、域)。
静态初始化只会在首次加载 Class 对象时初始化一次
—PS:static 修饰的东西属于类,只创建一次
显式的静态初始化
你可以将一组静态初始化动作放在类里面一个特殊的"静态子句"(有时叫做静态块)中。像下面这样:
public class Spoon {
static int i;
static {
i = 47;
}
}
这看起来像个方法,但实际上它只是一段跟在 static 关键字后面的代码块。与其他静态初始化动作一样,这段代码仅执行一次:当首次创建这个类的对象或首次访问这个类的静态成员(甚至不需要创建该类的对象)时。
非静态实例初始化
Java 提供了被称为实例初始化的类似语法,用来初始化每个对象的非静态变量。
可以使用它保证某些操作一定会发生,而不管哪个构造器被调 用
—PS:就是大括号包含的代码,不用 static 修饰
数组是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。数组是通过方括号下标操作符 [] 来定义和使用的。要定义一个数组引用,只需要在类型名加上方括号:
int[] a1;
或
int a1[];
不过前一种格式或许更合理,毕竟它表明类型是"一个 int 型数组"。
对于数组,初始化动作可以出现在代码的任何地方,但是也可以使用一种特殊的初始化表达式,它必须在创建数组的地方出现。这种特殊的初始化是由一对花括号括起来的值组成。这种情况下,存储空间的分配(相当于使用 new) 将由编译器负责。
int[] a1 = {1, 2, 3, 4, 5};
Java 数组计数也是从 0 开始的,所能使用的最大下标数是 length - 1。
动态数组创建
如果在编写程序时,不确定数组中需要多少个元素,那么该怎么办呢?你可以直接使用 new 在数组中 创建元素。
int[] a = new int[rand.nextInt(20)];
可变参数列表
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[]{47, (float) 3.14, 11.11});
printArray(new Object[]{"one", "two", "three"});
printArray(new Object[]{new A(), new A(), new A()});
}
}
输出:
47 3.14 11.11
one two three
A@15db9742 A@6d06d69c A@7852e922
你可能看到像上面这样编写的 Java 5 之前的代码,它们可以产生可变的参数列表。在 Java 5 中,这种期盼已久的特性终于添加了进来,就像在 printArray() 中看到的那样:
public class NewVarArgs {
static void printArray(Object... args) {
for (Object obj : args) {
System.out.print(obj + " ");
}
System.out.println();
}
public static void main(String[] args) {
// Can take individual elements:
printArray(47, (float) 3.14, 11.11);
printArray(47, 3.14F, 11.11);
printArray("one", "two", "three");
printArray(new A(), new A(), new A());
// Or an array:
printArray((Object[]) new Integer[]{1, 2, 3, 4});
printArray(); // Empty list is OK
}
}
有了可变参数,你就再也不用显式地编写数组语法了,当你指定参数时,编译器实际上会为你填充数组。
—PS:Object… args
Java 5 中添加了一个看似很小的特性 enum 关键字,它使得我们在需要群组并使用枚举类型集时,可以很方便地处理。
public enum Spiciness {
NOT, MILD, MEDIUM, HOT, FLAMING
}
要使用 enum,需要创建一个该类型的引用,然后将其赋值给某个实例:
public class SimpleEnumUse {
public static void main(String[] args) {
Spiciness howHot = Spiciness.MEDIUM;
System.out.println(howHot);
}
}
输出:
MEDIUM
尽管 enum 看起来像是一种新的数据类型,但是这个关键字只是在生成 enum 的类时,产生了某些编译器行为,因此在很大程度上你可以将 enum 当作其他任何类。事实上,enum 确实是类,并且具有自己的方法。
由于 switch 是在有限的可能值集合中选择,因此它与 enum 是绝佳的组合。注意,enum 的名称是如何能够倍加清楚地表明程序的目的的。
构造器,这种看起来精巧的初始化机制,应该给了你很强的暗示:初始化在编程语言中的重要地位。
因为构造器能保证进行正确的初始化和清理(没有正确的构造器调用,编译器就不允许创建对象),所以你就有了完全的控制和安全。
在 Java 中,垃圾回收器会自动地释放所有对象的内存,所以很多时候类似的清理方法就不太需要了(但是当要用到的时候,你得自己动手)。
自我学习总结: