Java编程思想——day 5
从现在开始,每天学习并记录。
2018/03/21
第五章 初始化与清理
- 初始化和清理(cleanup)是涉及安全的两个问题。
- C++引入了构造器(constructor)的概念,这是一个在创建对象时被自动调用的特殊方法。
- Java中也采用了构造器,并额外提供了“垃圾回收器”。对于不再使用的内存资源,垃圾回收器能自动将其释放。
1、用构造器确保初始化
- 在Java中,通过提供构造器,类的设计者可确保每个对象都会得到初始化。
- 调用构造器是编译器的责任;构造器采用与类相同的名称。
- 利用new创建对象时(new表达式返回了对新建对象的引用),编译器将会为对象分配存储空间,并调用相应的构造器。
- 不接受任何参数的构造器叫做默认构造器,又称为无参构造器。
- 构造器有助于减少错误,并使代码更易于阅读。在Java中,“初始化”和“创建”捆绑在一起,不能分离。
- 构造器是一种特殊类型的方法,因为它没有返回值。这与返回值为空(void)明显不同。
2、方法重载
- 同名方法参数类型列表不同,实现了方法重载。
- 同名方法参数顺序不同也可以实现重载,但一般不建议使用参数顺序不同实现重载,因为容易造成混乱。
- 基本类型能从一个“较小”的类型自动提升至一个“较大”的类型,此过程一旦牵涉到重载,可能会造成一些混淆。
- 如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型就会被提升。char型略有不同,如果无法找到恰好接受char参数的方法,就会把char直接提升至char型。
- 方法接受较小的基本类型作为参数。如果传入的实际参数较大,就得通过类型转换来执行窄化转换。如果不这样做,编译器就会报错。
- 方法重载不可以用方法的返回值进行实现。
3、默认构造器
- 默认构造器又称为“无参构造器”:没有参数,作用是创建一个默认的对象。如果你写的类中没有构造器,则编译器会自动帮你创建一个默认构造器。
- 但是,如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器。
4、this关键字
//: 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);//即Banana.peel(a,1);这是内部的表示形式
b.peel(2);//即Banana.peel(b,2);
}
} ///:~
- 为了实现“发送消息给对象”,编译器暗自把“所操作对象的引用”作为第一个参数传递给对象所调用的方法。
- 假设你希望在方法的内部获得对当前对象的引用,可以使用
this
关键字。 - 注意:this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。this的用法和其他对象引用并无不同。但要注意,如果在方法内部调用同一个类的另一个方法,就不必使用this ,直接调用即可。当前方法中的this用会自动应用于同一类中的其他方法(编译器会自动帮你添加)。只有当需要明确指定对当前对象的引用时,才需要使用this关键字。例如
return this;
- this关键字可以实现将当前对象传递给其他方法。
//: initialization/PassingThis.java
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());
}
} /* Output:
Yummy
*///:~
- 通常写this时,指当前对象或这个对象,并且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
*///:~
static
方法,即静态方法,就是没有this的方法。- 在
static
内部不能调用非静态方法(不绝对:如果你传递一个对象的引用到静态方法里(静态方法可以创建其自身的对象),然后通过这个引用(和this效果一样),你就可以调用非静态方法和访问非静态数据成员了,要想达到这样的效果,需要写一个非静态方法),非静态方法可调用static方法。 - 可以直接通过类本身调用static方法(不用创建对象),类似全局方法(Java禁止使用全局方法)。
5、清理:终结处理和垃圾回收
- 垃圾回收器只能回收由new分配的内存;
- Java允许在类中定义一个名为
finalize()
的方法。使用“本地方法”,在Java中调用非Java代码的方式。 finalize()
不是C++中的析构函数,Java未提供“析构函数”或相似的概念。- 注意:对象可能不被垃圾回收;垃圾回收并不等于“析构”;垃圾回收只与内存有关。
- 在java中不允许创建局部对象,必须使用new创建对象。
- 如果Java虚拟机(JVM)并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。
- 通常,不能指望
finalize()
,必须创建其他的“清理’方法,并且明确地调用它们。
//: initialization/TerminationCondition.java
// Using finalize() to detect an object that
// hasn't been properly cleaned up.
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");
// Normally, you'll also do this:
// super.finalize(); // Call the base-class version
}
}
public class TerminationCondition {
public static void main(String[] args) {
Book novel = new Book(true);
// Proper cleanup:
novel.checkIn();
// Drop the reference, forget to clean up:
new Book(true);
// Force garbage collection & finalization:
System.gc();//强制进行终结动作
}
} /* Output:
Error: checked out
*///:~
- 本例的终结条件是:所有的Beak对象在被当作垃圾回收前都应该被签入(check in)。但是在
main()
方法中,由于程序员的错误,有一本书未被签入。要是没有finalize()
来验证终结条件很难发现这种缺陷。
垃圾回收器工作方式: - 存储空间的释放会影响存储空间的分配——JVM的工作方式。
- Java的“堆指针”只是简单地移动到尚未分配的区域,C++是在堆上分配空间。内存页面调度会显著地影响性能。
- 垃圾回收器工作的时候,将一面回收空间,一面使堆中的对象紧凑排列,这样“堆指针”就可以很容易移动到更靠近传送带的开始处,也就尽量避免了页面错误。通过垃圾回收器对对象重新排列,实现了一种高速的、有无限空间可供分配的堆模型。
- “引用计数”是一种简单但速度很慢的垃圾回收技术。
- “交互自引用对象组”。
- 自适应的垃圾回收技术,它依据的思想是:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。这个引用链条可能会穿过数个对象层次。由此,如果从堆栈和静态存储区开始,遍历所有的引用,就能找到所有“活”的对象。对于发现的每个引用,必须追踪它所引用的对象,然后是此对象对象包含的所有引用,如此反复进行,直到“根源于堆栈和静态存储区的引用”所形成的网络全部被访问为止。
- 自适应的垃圾回收技术:停止-复制(stop-and-copy),即复制式回收器→→→→→标记-清扫(mark-and-sweep)。
- “自适应的、分代的、停止-复制、标记-清扫”式垃圾回收器。
- JVM用于提升速度的技术:与加载器操作有关的为:被称为“即时”(Just-In-Time, JIT)编译器的技术。
6、成员初始化
- 所有变量在使用前都能得到恰当的初始化。
7、构造器初始化
- 程序在运行时刻,可以调用方法或执行某些动作来确定初值,但无法阻止自动初始化(赋0值)的进行,然后再进行赋值操作(包括构造器的赋值操作)。
- 初始化顺序:在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化。例如:
//: initialization/OrderOfInitialization.java
// Demonstrates initialization order.
import static net.mindview.util.Print.*;
// 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()
*///:~
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)
*///:~
- 初始化的顺序是先静态对象(如果它们尚未因前面的对象创建过程而被初始化),而后是“非静态”对象。
- 注意:
main()
也是一个静态方法。 - 本例中,在这个特殊的程序中,所有类在
main()
开始之前就都被加载了。 - 实际情况通常并非如此,因为在典型的程序中不会像在本例中所做的那样,将所有的事物都通过static联系起来。
- 初始化的顺序:先静态对象(注意:只会被初始化一次),而后是非静态对象,
8、数组初始化
- 数组是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列,通过方括号下标操作符 “ [ ] ” 来定义和使用。
- 定义数组:
int [] a1;
或int a1[];
- 编译器不允许指定数组的大小。现在拥有的只不过是对数组的一个引用,而且也没给数组对象本身分配任何空间。
- 为了给数组创建相应的存储空间,必须写初始化表达式。数组的初始化动作可以出现在任何地方。特殊的初始化表达方式:由一对花括号括起来的值组成的,此时的存储空间的分配(等同于使用new)将由编译器负责。
- 未初始化而定义一个引用的目的是将一个数组赋值给另一个数组。
- 不确定数组里有多少个元素时,可以直接使用
new
在数组里创建元素。(注意:不能用new
创建单个的基本类型数据。但基本类型数组是可以直接用new
创建的 )
几种使用方法:
初始化并赋值:
//: initialization/ArraysOfPrimitives.java
import static net.mindview.util.Print.*;
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++)
print("a1[" + i + "] = " + a1[i]);
}
} /* Output:
a1[0] = 2
a1[1] = 3
a1[2] = 4
a1[3] = 5
a1[4] = 6
*///:~
利用new
初始化:
//: initialization/ArrayNew.java
// Creating arrays with new.
import java.util.*;
import static net.mindview.util.Print.*;
public class ArrayNew {
public static void main(String[] args) {
int[] a;
Random rand = new Random(47);
a = new int[rand.nextInt(20)];//或者int[] a = new int[rand.nextInt(20);进行定义并初始化
print("length of a = " + a.length);
print(Arrays.toString(a));
}
} /* Output:
length of a = 18
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
*///:~
创建一非基本类型的数组,相当于创建了一个引用数组。例如:整型的包装器类Integer:
//: initialization/ArrayClassObj.java
// Creating an array of nonprimitive objects.
import java.util.*;
import static net.mindview.util.Print.*;
public class ArrayClassObj {
public static void main(String[] args) {
Random rand = new Random(47);
Integer[] a = new Integer[rand.nextInt(20)];
print("length of a = " + a.length);
for(int i = 0; i < a.length; i++)
a[i] = rand.nextInt(500); // Autoboxing
print(Arrays.toString(a));
}
} /* Output: (Sample)
length of a = 18
[55, 193, 361, 461, 429, 368, 200, 22, 207, 288, 128, 51, 89, 309, 278, 498, 361, 20]
*///:~
//注意:即使使用new创建数组之后:
//Integer[] a=new Integer[rand.nextInt(20)];
//它还只是一个对象引用,并且直到创建一个新的Integer对象(在本例中是通过自动包装机制创建的),并把对象赋值给引用,初始化进程才算结束:
//a[i]=rand.nextInt(500);
利用花括号括起来的列表初始化对象数组:两种形式:
//: initialization/ArrayInit.java
// Array initialization.
import java.util.*;
public class ArrayInit {
public static void main(String[] args) {
Integer[] a = {
new Integer(1),
new Integer(2),
3, // Autoboxing
};
Integer[] b = new Integer[]{
new Integer(1),
new Integer(2),
3, // Autoboxing
};
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(b));
}
} /* Output:
[1, 2, 3]
[1, 2, 3]
*///:~
在这两种形式中,初始化列表的最后一个逗号都是可选的(这一特性使维护长列表变得更容易)。
第一种方式比较受限:只能用于数组被定义的地方;
第二种方式可以在任何地方调用,甚至在方法调用的内部,例如,创建一个String
对象数组,将其传递给另一个main()
方法,以提供参数,用来替换传递给该main()
方法的命令行参数:
//: initialization/DynamicArray.java
// Array initialization.
public class DynamicArray {
public static void main(String[] args) {
Other.main(new String[]{ "fiddle", "de", "dum" });
}
}
class Other {
public static void main(String[] args) {
for(String s : args)
System.out.print(s + " ");
}
} /* Output:
fiddle de dum
*///:~
- 可变参数列表:可以创建以Object数组为参数的方法,并调用:
//: initialization/VarArgs.java
// Using array syntax to create variable argument lists.
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
*///:~
打印对象的内容时,如果没有定义toString()
方法时,默认输出类的名字和对象的地址。
可变参数列表:
//: initialization/NewVarArgs.java
// Using array syntax to create variable argument lists.
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(new Integer(47), new Float(3.14),
new Double(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
}
} /* Output: (75% match)
47 3.14 11.11
47 3.14 11.11
one two three
A@1bab50a A@c3c749 A@150bd4d
1 2 3 4
*///:~
getClass()
方法属于Object的一部分。- 使用可变参数列表不依赖于自动包装机制,实际上使用的是基本类型。
9、枚举类型
enum
关键字。- 枚举类型的实例为常量,因此需要按照命名惯例,他们都用大写字母表示(如果在一个名字中有多个单词,用下划线将其隔开)。
- 为了使用enum,需要创建一个该类的引用,并将其赋值给某个实例。
- 在创建enum时,编译器会自动添加一些有用的特性,例如:
toString()
方法,显示某个enum实例的名字;ordinal()
方法,表示某个特定enum常量的声明顺序(从0开始计数);static values()
方法,用来按照enum常量的声明顺序,产生由这些常量值构成的数组。
枚举定义:
//: initialization/Spiciness.java
public enum Spiciness {
NOT, MILD, MEDIUM, HOT, FLAMING
} ///:~
//: initialization/EnumOrder.java
public class EnumOrder {
public static void main(String[] args) {
for(Spiciness s : Spiciness.values())
System.out.println(s + ", ordinal " + s.ordinal());
}
} /* Output:
NOT, ordinal 0
MILD, ordinal 1
MEDIUM, ordinal 2
HOT, ordinal 3
FLAMING, ordinal 4
*///:~
- 把enum当做类进行使用:可以在
switch
语句中使用
//: initialization/Burrito.java
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.");
}
}
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();
}
} /* Output:
This burrito is not spicy at all.
This burrito is a little hot.
This burrito is maybe too hot.
*///:~
可以将enum当做另外一种创建数据类型的方式,然后直接将所得到的类型拿来使用。