第五章 初始化与清理
初始化和清理( cleanup)正是涉及安全的两个问题。许多C程序的错误都源于程序员忘记初始化变量。特别是在使用程序库时,如果用户不知道如何初始化库的构件(或者是用户必须进行初始化的其他东西),更是如此。清理也是一个特殊问题,当使用完一个元素时,它对你也就不会有什么影响了,所以很容易把它忘记。这样一来,这个元素占用的资源就会一直得不到释放,结果是资源(尤其是内存)用尽。
Java中采用了构造器的概念,并额外提供了“垃圾回收器”对于不再使用的内存资源,垃圾回收器能自动将其释放。
用构造器来确保初始化
可以假想为编写的每个类都定义一个 initialize()
方法。该方法的名称提醒你在使用其对象之前,应首先调用 initialize()
。然而,这同时意味着用户必须记得自己去调用此方法。在Java中,通过提供构造器,类的设计者可确保毎个对象都会得到初始化。创建对象时,如果其类具有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行。
以下就是带有构造器的简单类:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/26 16:06
*/
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文档中通常使用术语无参构造器,但是默认构造器在Java出现之前已经使用许多年了,但是和其他方法一样,构造器也能带有形式参数,以便指定如何创建对象。对上述例子稍加修改,即可使构造器接受一个参数:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/26 16:12
*/
class Rock2{
Rock2(int i){
System.out.print("Rock"+i+" ");
}
}
public class SimpleConstructor2 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Rock2(i);
}
}
}
Rock0 Rock1 Rock2 Rock3 Rock4 Rock5 Rock6 Rock7 Rock8 Rock9
有了构造器形式参数,就可以在初始化对象时提供实际参数。例如,假设类Tree
有一个构造器,它接受一个整型变量来表示树的高度,就可以这样创建一个Tree
对象:
Tree t = new Tree(12): //12-foot tree
如果 Tree(int)
是Tree
类中唯一的构造器,那么编译器将不会允许你以其他任何方式创建Tree
对象。
构造器有助于减少错误,并使代码更易于阅读。从概念上讲,“初始化”与“创建”是彼此独立的,然而在上面的代码中,你却找不到对 initialize()
方法的明确调用。在Java中,“初始化”和“创建”捆绑在一起,两者不能分离。
构造器是一种特殊类型的方法,因为它没有返回值。这与返回值为空(void)明显不同。对于空返回值,尽管方法本身不会自动返回什么,但仍可选择让它返回别的东西。
构造器则不会返回任何东西,你别无选择(new表达式确实返回了对新建对象的引用,但构造器本身并没有任何返回值)。假如构造器具有返回值,并且允许人们自行选择返回类型,那么势必得让编译器知道该如何处理此返回值。
练习1:(1)创建一个类,它包含一个未初始化的 String引用。验证该引用被Java初始化成了null.
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/26 16:17
*/
class Tester{
String s;
}
public class StringTest {
public static void main(String[] args) {
System.out.println(new Tester().s);
}
}
练习2:(2)创建一个类,它包含一个在定义时就被初始化了的 String域,以及另一个通过构造器初始化的 String域。这两种方式有何差异?
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/26 16:20
*/
class Tester2{
Tester2(){
str3 = "goodbye-";
}
String str1 = "123";
String str2;
String str3;
}
public class StringTest2 {
public static void main(String[] args) {
Tester2 tester2 = new Tester2();
System.out.println("tester2.str1 = "+tester2.str1);
System.out.println("tester2.str2 = "+tester2.str2);
System.out.println("tester2.str3 = "+tester2.str3);
}
}
tester2.str1 = 123
tester2.str2 = null
tester2.str3 = goodbye-
输出结果可得:
在定义时就被初始化了的String
域,在对象被实例化时引用str1
被赋值一次
另一个通过构造器初始化的String
域,在对象被实例化时引用str3
被赋值两次,第一次赋值 null
,在构造方法内再进行第二次赋值goodbye-
.
方法重载
在Java(和C++)里,构造器是强制重载方法名的另一个原因。既然构造器的名字已经由类名所决定,就只能有一个构造器名。那么要想用多种方式创建一个对象该怎么办呢?假设你要创建一个类,既可以用标准方式进行初始化,也可以从文件里读取信息来初始化。这就需要两个构造器:一个默认构造器,另一个取字符串作为形式参数一一该字符串表示初始化对象所需的文件名称。由于都是构造器,所以它们必须有相同的名字,即类名。为了让方法名相同而形式参数不同的构造器同时存在,必须用到方法重载。同时,尽管方法重载是构造器所必需的但它亦可应用于其他方法,且用法同样方便。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/26 16:57
*/
class Tree{
int height;
Tree(){
System.out.println("Planting a seeding.");
height= 0;
}
Tree(int initialHeight){
height = initialHeight;
System.out.println("Create new Tree that is "+height+" feet tall.");
}
void info(){
System.out.println("Tree is "+height+" feet tall.");
}
void info(String s){
System.out.println(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");
}
new Tree();
}
}
Create new Tree that is 0 feet tall.
Tree is 0 feet tall.
overloaded method: Tree is 0 feet tall.
Create new Tree that is 1 feet tall.
Tree is 1 feet tall.
overloaded method: Tree is 1 feet tall.
Create new Tree that is 2 feet tall.
Tree is 2 feet tall.
overloaded method: Tree is 2 feet tall.
Create new Tree that is 3 feet tall.
Tree is 3 feet tall.
overloaded method: Tree is 3 feet tall.
Create new Tree that is 4 feet tall.
Tree is 4 feet tall.
overloaded method: Tree is 4 feet tall.
Planting a seeding.
创建Tree
对象的时候,既可以不含参数,也可以用树的高度当参数。前者表示一棵树苗,后者表示已有一定高度的树木。要支持这种创建方式,得有一个默认构造器和一个采用现有高度作为参数的构造器。
或许你还想通过多种方式调用info()
方法。例如,如果想显示额外信息,可以用 info(String)
方法;没有的话就用info()
。要是对明显相同的概念使用了不同的名字,那一定会让人很纳闷。好在有了方法重载,可以为两者使用相同的名字。
区分重载方法
要是几个方法有相同的名字,Java如何才能知道你指的是哪一个呢?其实规则很简单:每个重载的方法都必须有一个独一无二的参数类型列表。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/26 17:10
*/
public class OverloadingOrder {
static void f(String s,int i){
System.out.println("String: "+s+", int: "+i);
}
static void f(int i,String s){
System.out.println("String: "+s+", int: "+i);
}
public static void main(String[] args) {
f("String first",11);
f(99,"Int first");
}
}
String: String first, int: 11
String: Int first, int: 99
涉及基本类型的重载
基本类型能从一个“较小”的类型自动提升至一个“较大”的类型,此过程一且牵涉到重载,可能会造成一些混淆。以下例子说明了将基本类型传递给重载方法时发生的情况:
import static net.mindview.util.Print.*;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/26 17:20
*/
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 testDoouble(){
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.testDoouble();
}
}
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)
我们会发现常数值5
被当作int
值处理,所以如果有某个重载方法接受int
型参数,它就会被调用。至于其他情况,如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型就会被提升。char
型略有不同,如果无法找到恰好接受char
参数的方法,就会把char
直接提升至int
型。而如果方法接受较小的基本类型作为参数。如果传入的实际参数较大,就得通过类型转换来执行窄化转换。如果不这样做,编译器就会报错。
默认构造器
默认构造器(又名“无参”构造器)是没有形式参数的一它的作用是创建一个默认对象”。如果你写的类中没有构造器,则编译器会自动帮你创建一个默认构造器。例如:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/27 14:49
*/
class Bird{}
public class DefaultConstructor {
public static void main(String[] args) {
Bird b = new Bird(); // Default!!~
}
}
表达式new Bird()
行创建了一个新对象,并调用其默认构造器一一即使你没有明确定义它。没有它的话,就没有方法可调用,就无法创建对象。但是,如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/27 14:52
*/
class Bird2{
Bird2(int i){}
Bird2(double d){}
}
public class NoSynthesis {
public static void main(String[] args) {
// Bird2 b = new Bird2(); //No defalut
Bird2 b2 = new Bird2(1);
Bird2 b3 = new Bird2(1.0);
}
}
要是你这样写new Bird2()
编译器就会报错:没有找到匹配的构造器。这就好比,要是你没有提供任何构造器,编译器会认为"你需要一个构造器,让我给你制造一个吧";但假如你已写了一个构造器,编译器则会认为“啊,你已写了一个构造器,所以你知道你在做什么;你是刻意省略了默认构造器。”
练习3:(1)创建一个带默认构造器(即无参构造器)的类,在构造器中打印一条消息。为这个类创建一个对象。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/27 15:00
*/
class Cons{
Cons(){
System.out.println("无参构造器.");
}
}
public class PrintDefalut {
public static void main(String[] args) {
Cons cons = new Cons();
}
}
练习4:(1)为前一个练习中的类添加一个重载构造器,令其接受一个字符申参数,并在构造器中把你自己的消息和接收的参数一起打印出来。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/27 15:00
*/
class Cons{
Cons(){
System.out.println("无参构造器.");
}
Cons(String str){
System.out.println("有参构造器.");
System.out.println(str);
}
}
public class PrintDefalut {
public static void main(String[] args) {
Cons cons = new Cons();
Cons cons1 = new Cons("hello");
}
}
练习5:(2)创建一个名为Dog的类,它具有重载的 bark()方法。此方法应根据不同的基本数据类型进行重载,并根据被调用的版本,打印出不同类型的狗吠( barking)、咆哮( howling)等信息。编写 main()来调用所有不同版本的方法。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/27 15:17
*/
class Dog{
void bark(){System.out.println("quiet");}
void bark(char c){
System.out.println("cawcaw");
}
void bark(byte b){
System.out.println("barkbark");
}
void bark(short s){
System.out.println("slurpslurp");
}
void bark(int i){
System.out.println("iyow");
}
void bark(long l){
System.out.println("lalala");
}
void bark(float f){
System.out.println("fififoofum");
}
void bark(double d){
System.out.println("dodo");
}
}
public class DogTalk {
public static void main(String[] args) {
Dog dog = new Dog();
char c = 'c';
byte b = 0;
short s = 0;
dog.bark();
dog.bark(c);
dog.bark(b);
dog.bark(s);
dog.bark(1);
dog.bark(1L);
dog.bark(1.0f);
dog.bark(1.0);
}
}
quiet
cawcaw
barkbark
slurpslurp
iyow
lalala
fififoofum
dodo
练习6:(1)修改前一个练习的程序,让两个重载方法各自接受两个类型的不同的参数,但二者顺序相反。验证其是否工作。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/27 15:17
*/
class Dog{
void bark(){System.out.println("quiet");}
void bark(char c){
System.out.println("cawcaw");
}
void bark(byte b){
System.out.println("barkbark");
}
void bark(short s){
System.out.println("slurpslurp");
}
void bark(int i){
System.out.println("iyow");
}
void bark(long l){
System.out.println("lalala");
}
void bark(float f){
System.out.println("fififoofum");
}
void bark(double d){
System.out.println("dodo");
}
void bark(char c,int i){
System.out.println("caw-iyow");
}
void bark(int i,char c){
System.out.println("iyow-caw");
}
}
public class DogTalk {
public static void main(String[] args) {
Dog dog = new Dog();
char c = 'c';
byte b = 0;
short s = 0;
// dog.bark();
// dog.bark(c);
// dog.bark(b);
// dog.bark(s);
// dog.bark(1);
// dog.bark(1L);
// dog.bark(1.0f);
// dog.bark(1.0);
dog.bark(1,c);
dog.bark(c,1);
}
}
练习7:(1)创建一个没有构造器的类,并在main()中创建其对象,用以验证编译器是否真的自动加入了默认构造器。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/27 15:26
*/
class NoDeCons{
void bark(){
System.out.println("woof.");
}
}
public class NoDefalutCons {
public static void main(String[] args) {
NoDeCons noDeCons = new NoDeCons();
noDeCons.bark();
}
}
this关键字
如果有同一类型的两个对象,分别是a
和b
。你可能想知道,如何才能让这两个对象都能调用peel()
方法呢:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/27 15:48
*/
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()
方法,它如何知道是被还是被b所调用的呢?为了能用简便、面向对象的语法来编写代码即“发送消息给对象”,编译器做了一些幕后工作。它暗自把“所操作对象的引用”作为第一个参数传递给 peel()
。所以上述两个方法的调用就变成了这样:
Banana peel(a, 1);
Banana peel(b, 2);
这是内部的表示形式。我们并不能这样书写代码并试图通过编译;但这种写法的确能帮你了解实际所发生的事情,假设你希望在方法的内部获得对当前对象的引用。由于这个引用是由编译器“偷偷”传入的,所以没有标识符可用。但是,为此有个专门的关键字:this
.
this
关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。this
的用法和其他对象引用并无不同。但要注意,如果在方法内部调用同一个类的另ー个方法,就不必使用this
,直接调用即可。当前方法中的this
用会自动应用于同一类中的其他方法。所以可以这样写代码:
public class Apricot{
void pick(){}
void pit(){
pick();
//....
}
}
在pit()
内部,你可以写this. pick()
,但无此必要。编译器能帮你自动添加。只有当需要明确指出对当前对象的引用时,才需要使用this
关键字。例如,当需要返回对当前对象的引用时,就常常在retrun
语句里这样写:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/27 16:06
*/
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
关键字对于将当前对象传递给其他方法也很有用:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/27 16:17
*/
class Person{
public void eat(Apple apple){
Apple peeled = apple.getPeeled();
System.out.println("yummy~");
}
}
class Peeler{
static Apple peel(Apple apple){
return apple;
}
}
class Apple{
Apple getPeeled(){
return Peeler.peel(this);
}
}
public class PassingThis {
public static void main(String[] args) {
new Person().eat(new Apple());
}
}
Apple
需要调用 Peeler.peel()
方法,它是一个外部的工具方法,将执行由于某种原因而必须放在 Apple
外部的操作(也许是因为该外部方法要应用于许多不同的类,而你却不想重复这些代码)。为了将其自身传递给外部方法, Apple
必须使用this
关健字。
练习8:(1)编写具有两个方法的类,在第一个方法内调用第二个方法两次:第一次调用时不使用this关键字,第二次调用时使用this关键字一这里只是为了验证它是起作用的,你不应该在实践中使用这种方式。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/27 16:51
*/
class Doc{
void intubate(){
System.out.println("prepare patient.");
laryngoscopy();
this.laryngoscopy();
}
void laryngoscopy(){
System.out.println("use laryngoscopy.");
}
}
public class PassingThisEx {
public static void main(String[] args) {
new Doc().intubate();
}
}
prepare patient.
use laryngoscopy.
use laryngoscopy.
在构造器中调用构造器
可能为一个类写了多个构造器,有时可能想在一个构造器中调用另一个构造器,以避免重复代码。可用this
关键字做到这一点。通常写this
的时侯,都是指“这个对象”或者“当前对象”,而且它本身表示对当前对象的引用。
在构造器中,如果为this
添加了参数列表,那么就有了不同的含义。这将产生对符合此参数列表的某个构造器的明确调用;这样,调用其他构造器就有了直接的途径:
import static net.mindview.util.Print.*;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/28 15:53
*/
public class Flower {
int petalCount = 0;
String s = "initial value";
Flower(int petals){
petalCount = petals;
print("Constructor w/ int arg only,petalCount = "+petals);
}
Flower(String ss){
s = ss;
print("Constructor w/ String arg only,petalCount s= "+ss);
}
Flower(String s,int petals){
this(petals);
// this(s); //Can not 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();
}
}
构造器 Flower( String s,int petals
)表明:尽管可以用this
调用一个构造器,但却不能调用两个此外,必须将构造器调用置于最起始处,否则编译器会报错。
这个例子也展示了this
的另一种用法。由于参数s
的名称和数据成员s
的名字相同,所以会产生歧义。使用 this.s
来代表数据成员就能解决这个问题。在Java程序代码中经常出现这种写法,本书中也常这么写。
printPetalCount()
方法表明,除构造器之外,编译器禁止在其他任何方法中调用构造器。
练习9:(1)编写具有两个(重载)构造器的类,并在第一个构造器中通过this调用第二个构造器。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/28 16:23
*/
class Docs{
Docs(int i){
this("MD");
int yearsTraning = i;
System.out.println("New docs with " + i + " years of training");
}
Docs(String s){
String degree= s;
System.out.println("New docs with "+s+" degree");
}
void intubate(){
System.out.println("prepare patient");
laryngoscopy();
}
void laryngoscopy(){
System.out.println("use laryngoscopy.");
}
}
public class OverloadedConstructors {
public static void main(String[] args) {
new Docs(8).intubate();
}
}
static的含义
了解this
关健字之后,就能更全面地理解 static
(静态)方法的含义。 static
方法就是没有this
的方法。在 static方法的内部不能调用非静态方法,反过来倒是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用 static
方法。这实际上正是 static
方法的主要用途。它很像全局方法。Java中禁止使用全局方法,但你在类中置入 static
.方法就可以访问其他 static
方法和 static
域。
清理:终结处理和垃圾回收
Java有垃圾回收器负责回收无用对象占据的内存资源。但也有特殊情况:假定你的对象(并非使用new
)获得了一块“特殊”的内存区域,由于垃圾回收器只知道释放那些经由new
分配的内存,所以它不知道该如何释放该对象的这块“特殊”内存。为了应对这种情况,Java允许在类中定义一个名为 finalize()
的方法。它的工作原理“假定”是这样的:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其 finalize()
方法,并且在下一次垃圾回收动作发生时,オ会真正回收对象占用的内存。所以要是你打算使用finalize()
,就能在垃圾回收时刻做一些重要的清理工作。
finalize()的用途何在
垃圾回收只与内存有关
也就是说,使用垃圾回收器的唯一原因是为了回收程序不再使用的内存。所以对于与垃圾回收有关的任何行为来说(尤其是 finalize()
方法),它们也必须同内存及其回收有关。
但这是否意味着要是对象中含有其他对象, finalize
就应该明确释放那些对象呢?不,无论对象是如何创建的,垃圾回收器都会负责释放对象占据的所有内存。这就将对 finalize()
的需求限制到一种特殊情况,即通过某种创建对象方式以外的方式为对象分配了存储空间。
看来之所以要用finalize()
,是由于在分配内存时可能采用了类似C语言的做法,这种情况主要发生在使用**“本地方法”**的情况下,本地方法是一种在Java中调用非Java代码的方式。
终结条件
通常,不能指望 finalize()
,必须创建其他的“清理”方法,并且明确地调用它们。看来,finalize()
只能存在于程序员很难用到的一些晦涩用法里了。不过, finalize()
还有一个有趣的用法,它并不依赖于毎次都要对 finalize()
进行调用,这就是对象终结条件的验证。
以下是个简单的例子,用来示范finalize()
可能使用的方式:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/28 18:06
*/
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");
// super.finalize();
}
}
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();
}
}
本例的终结条件是:所有的Book
对象在被当作垃圾回收前都应该被签入( checkin
)。但在main()
方法中,由于程序员的错误,有一本书未被签入。要是没有 finalize
来验证终结条件,将很难发现这种缺陷。
注意, System.gc()
用于强制进行终结动作。即使不这么做,通过重复地执行程序(假设程序将分配大量的存储空间而导致垃圾回收动作的执行),最终也能找出错误的Book
对象。
练习10:(2)编写具有 finalize方法的类,并在方法中打印消息。在main()
中为该类创建个对象。试解释这个程序的行为。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/29 15:39
*/
class WebBank{
boolean loggedIn = false;
WebBank(boolean logStatus){
loggedIn= logStatus;
}
void logIn(){
loggedIn = true;
}
void logOut(){
loggedIn = false;
}
protected void finalize(){
if(loggedIn)
System.out.println("Err: still logged in.");
// super.finalize();
}
}
public class TerminationConditionEx {
public static void main(String[] args) {
WebBank webBank1 = new WebBank(true);
WebBank webBank2 = new WebBank(true);
webBank1.logOut();
new WebBank(true);
System.gc();
}
}
练习11:(4)修改前一个练习的程序,让你的 finalize()总会被调用。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/7/29 15:39
*/
class WebBank{
boolean loggedIn = false;
WebBank(boolean logStatus){
loggedIn= logStatus;
}
void logOut(){
loggedIn = false;
}
protected void finalize(){
if(loggedIn)
System.out.println("Err: still logged in.");
// super.finalize();
}
}
public class TerminationConditionEx {
public static void main(String[] args) {
WebBank webBank1 = new WebBank(true);
WebBank webBank2 = new WebBank(true);
new WebBank(true);
webBank1.logOut();
System.out.println("Try 1:");
System.runFinalization();
System.out.println("Try 2:");
Runtime.getRuntime().runFinalization();
System.out.println("Try 3:");
System.gc();
System.out.println("Try 4:");
System.runFinalizersOnExit(true); // using deprecated since 1.1 method
}
}
练习12:(4)编写名为Tank的类,此类的状态可以是“满的”或“空的”。其终结条件是:对象被清理时必须处于空状态。请编写 finalize以检验终结条件是否成立。在 main()中测试Tank可能发生的几种使用方式。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 13:52
*/
class Tank{
int howFull= 0;
Tank(){
this(0);
}
Tank(int fullness){
howFull = fullness;
}
void sayHowFull(){
if(howFull==0){
System.out.println("Tank is empty.");
}else {
System.out.println("Tank filling status = "+howFull);
}
}
void empty(){
howFull = 0;
}
protected void finalize(){
if(howFull != 0){
System.out.println("ERROR: Tank not empty.");
}
// super.finalize();
}
}
public class TankTest {
public static void main(String[] args) {
Tank tank1 = new Tank();
Tank tank2 = new Tank(3);
Tank tank3 = new Tank(5);
tank2.empty(); //Proper cleanup: empty tank before going home.
/**
* Drop the reference,forgot to cleanup
*/
new Tank(6);
System.out.println("Check tanks:");
System.out.println("tank1: ");
tank1.sayHowFull();
System.out.println("tank2: ");
tank2.sayHowFull();
System.out.println("tank3: ");
tank3.sayHowFull();
System.out.println("first forced gc(): ");
System.gc();
// Force finalization on exit but using method
// deprecated since JDK 1.1:
System.out.println("try deprecated runFinalizersOnExit(true):");
System.runFinalizersOnExit(true);
System.out.println("last forced gc():");
System.gc();
}
}
Check tanks:
tank1:
Tank is empty.
tank2:
Tank is empty.
tank3:
Tank filling status = 5
first forced gc():
try deprecated runFinalizersOnExit(true):
ERROR: Tank not empty.
last forced gc():
ERROR: Tank not empty.
成员初始化
Java中类的每个基本类型数据成员保证都会有一个初始值,即使没有给出数据成员的初始值,但它们确实有(char
值为0,所以为空白)。在类里定义一个对象引用时,如果不对其进行初始化,此引用就会获得一个初始值null
。
指定初始化
除了直接在类里面定义成员变量并为其直接赋值的方式,我们还可以通过其他的方式来对成员变量进行赋值:
通过调用某个方法来提供初始值
public class MethodInit{
int i = f();
int f(){
return 11;
}
}
这个方法可以带有参数,但是参数必须得是已经进初始化了的:
public class MethodInit2{
int i = f();
int j = g(i);
int f(){
return 11;
}
int g(int n){
return n*20;
}
}
构造器初始化
初始化顺序
在类内部,变量定义的先后顺序决定了初始化的顺序。即使变量变量定义散布于方法定义之间,它们仍旧会在任何方法(包括构造器)被调用之前得到初始化,例如:
import static net.mindview.util.Print.*;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 14:28
*/
class Window{
Window(int marker){
print("Window("+marker+")");
}
}
class House{
Window w1 = new Window(1);
House(){
print("Hose()");
w3 = new Window(33);
}
Window w2 =new Window(2);
void f(){print("f()");}
Window w3 = new Window(3);
}
public class OrderOfInitialization {
public static void main(String[] args) {
House house = new House();
house.f();
}
}
在 House
类中,故意把几个 Window
对象的定义散布到各处,以证明它们全都会在调用构造器或其他方法之前得到初始化。此外,w3
在构造器内再次被初始化。
由输出可见,w3
这个引用会被初始化两次:一次在调用构造器前,一次在调用期间(第1次引用的对象将被丢弃,并作为垃圾回收)。试想,如果定义了一个重载构造器,它没有初始化w3
;同时在w3
的定义里也没有指定默认值,那会产生什么后果呢?所以尽管这种方法似乎效率不高,但它的确能使初始化得到保证。
静态数据的初始化
无论创建多少个对象,静态数据都只占用一份存储区域。 static
关键字不能应用于局部变量,因此它只能作用于域。如果一个域是静态的基本类型域,且也没有对它进行初始化,那么它就会获得基本类型的标准初值;如果它是一个对象引用,那么它的默认初始化值就是null
。
如果想在定义处进行初始化,采取的方法与非静态数据没什么不同。
import javafx.scene.control.Tab;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 14:40
*/
class Bowl{
Bowl(int maker){
System.out.println("Bowl("+maker+")");
}
void f1(int maker){
System.out.println("f1("+maker+")");
}
}
class Table{
static Bowl bowl1 =new Bowl(1);
Table(){
System.out.println("Table()");
bowl2.f1(1);
}
void f2(int maker){
System.out.println("f2("+maker+")");
}
static Bowl bowl2 = new Bowl(2);
}
class Cupboard{
Bowl bowl3 = new Bowl(3);
static Bowl bowl4 = new Bowl(4);
Cupboard(){
System.out.println("Cupboard()");
bowl4.f1(2);
}
void f3(int maker){
System.out.println("f3("+maker+")");
}
static Bowl bowl5 = new Bowl(5);
}
public class StaticInit {
public static void main(String[] args) {
System.out.println("Creating new Cupboard() in main");
new Cupboard();
System.out.println("Creating new Cupboard() in main");
new Cupboard();
table.f2(1);
cupboard.f3(1);
}
static Table table = new Table();
static Cupboard cupboard = new Cupboard();
}
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)
Bowl
类使得看到类的创建,而 Table
类和 Cupboard
类在它们的类定义中加入了Bowl
类型的静态数据成员。注意,在静态数据成员定义之前, Cupboard
类先定义了一个Bowl
类型的非静态数据成员b3
。
由输出可见,静态初始化只有在必要时刻才会进行。如果不创建 Table
对象,也不引用Table.b1
或 Table.b2
,那么静态的 Bowl
b1
和b2
永远都不会被创建。只有在第一个 Table
对象被创建(或者第一次访问静态数据)的时候,它们オ会被初始化。此后,静态对象不会再次被初始化。
初始化的顺序是先静态对象(如果它们尚未因前面的对象创建过程而被初始化),而后是“非静态”对象。从输出结果中可以观察到这一点。要执行main()
(静态方法),必须加载StaticInit
类,然后其静态域 table
和 cupboard
被初始化,这将导致它们对应的类也被加载,并且由于它们也都包含静态的Bowl
对象,因此Bowl
随后也被加载。这样,在这个特殊的程序中的所有类在main()
开始之前就都被加载了。实际情况通常并非如此,因为在典型的程序中,不会像在本例中所做的那样,将所有的事物都通过 static
联系起来。
总结一下对象的创建过程,假设有个名为Dog
的类:
1.即使没有显式地使用
static
关键字,构造器实际上也是静态方法。因此,当首次创建类型为Dog
的对象时(构造器可以看成静态方法),或者Dog
类的静态方法/静态域首次被访问时,Java解释器必须査找类路径,以定位Dog.class
文件
2.然后载人Dog.class
(后面会学到,这将创建一个Class
对象),有关静态初始化的所有动作都会执行。因此,静态初始化只在Class
对象首次加载的时候进行一次。
3.当用new Dog()
创建对象的时候,首先将在堆上为Dog
对象分配足够的存储空间。
4.这块存储空间会被清零,这就自动地将Dog
对象中的所有基本类型数据都设置成了默认值(对数字来说就是0,对布尔型和字符型也相同),而引用则被设置成了null
.
5.执行所有出现于字段定义处的初始化动作。
6.执行构造器。正如将在第7章所看到的,这可能会牵涉到很多动作,尤其是涉及继承的时候。
显示的静态初始化
Java允许将多个静态初始化动作组织成一个特殊的“静态子句”(有时也叫做“静态块”)。就像下面这样:
public class Spoon{
static int i;
static{
i= 47;
}
}
尽管上面的代码看起来像个方法,但它实际只是一段跟在 static
关键字后面的代码。与其他静态初始化动作一样,这段代码仅执行一次:当首次生成这个类的一个对象时,或者首次访问属于那个类的静态数据成员时(即便从未生成过那个类的对象)。例如:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 15:06
*/
class Cup{
Cup(int marker){
System.out.println("Cup("+marker+")");
}
void f(int marker){
System.out.println("f("+marker+")");
}
}
class Cups{
static Cup cup1;
static Cup cup2;
static {
cup1 = new Cup(1);
cup2 = new Cup(2);
}
Cups(){
System.out.println("Cups()");
}
}
public class ExplicitStatic {
public static void main(String[] args) {
System.out.println("Inside main()");
Cups.cup1.f(99); // (1)
}
// static Cups cups1 = new Cups(); // (2)
// static Cups cups2 = new Cups(); // (2)
}
无论是通过标为(1)的那行代码访问静态的cup1
对象,还是把标为(1)的行注释掉,让它去运行标为(2)的那行代码(即解除标为(2)的行的注释),Cups
的静态初始化动作都会得到执行。如果把标为(1)和(2)的行同时注释掉,Cups
的静态初始化动作就不会进行,就像在输出中看到的那样。此外,激活一行还是两行标为(2)的代码(即解除注释)都无关紧要,静态初始化动作只进行一次。
练习14:(1)编写一个类,拥有两个静态字符串域,其中ー个在定义处初始化,另一个在静态块中初始化。现在,加入一个静态方法用以打印出两个字段值。请证明它们都会在被使用之前完成初始化动作。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 15:14
*/
class Go{
static String s1 = "run";
static String s2 ,s3;
static {
s2="drive car";
s3 = "fly plane";
System.out.println("s2s & s3 init.");
}
static void how(){
System.out.println(s1 + " or "+s2+" or "+s3);
}
Go(){
System.out.println("Go()");
}
}
public class ExplicitStaticEx {
public static void main(String[] args) {
System.out.println("Inside main()");
Go.how();
System.out.println("Go.s1: "+Go.s1);
}
static Go g1 = new Go();
static Go g2 = new Go();
}
非静态实例初始化
Java中也有被称为实例初始化的类似语法,用来初始化每一个对象的非静态变量。例如:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 15:21
*/
class Mug{
Mug(int marker){
System.out.println("Mug("+marker+")");
}
void f(int marker){
System.out.println("f("+marker+")");
}
}
public class Mugs {
Mug mug1;
Mug mug2;
{
mug1 = new Mug(1);
mug2 = new Mug(2);
System.out.println("mug1 & mug2 init.");
}
Mugs(){
System.out.println("Mugs()");
}
Mugs(int i){
System.out.println("Mugs(int)");
}
public static void main(String[] args) {
System.out.println("Inside main()");
new Mugs();
System.out.println("new Mugs completed");
new Mugs(1);
System.out.println("new Mugs(1) completed");
}
}
Inside main()
Mug(1)
Mug(2)
mug1 & mug2 init.
Mugs()
new Mugs completed
Mug(1)
Mug(2)
mug1 & mug2 init.
Mugs(int)
new Mugs(1) completed
你可以看到实例化句子:
{
mug1 = new Mug(1);
mug2 = new Mug(2);
System.out.println("mug1 & mug2 init.");
}
看起来它与静态初始化子句一模一样,只不过少了 static
关键字。这种语法对于支持“匿名内部类”(参见第10章)的初始化是必须的,但是它也使得你可以保证无论调用了哪个显式构造器,某些操作都会发生。从输出中可以看到实例初始化子句是在两个构造器之前执行的。
练习15:(1)编写一个含有字符串域的类,并采用实例初始化方式进行初始化
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 15:30
*/
class Testers {
String s;
{
s = "Initializing string in Testers";
System.out.println(s);
}
Testers() {
System.out.println("Testers()");
}
}
public class InstanceClauseTest {
public static void main(String[] args) {
new Testers();
}
}
数组初始化
练习16:(1)创建一个 String对象数据,并为每一个元素都赋值一个 String。用for循环来打印该数组。
public class StringArrayEx16 {
public static void main(String[] args) {
String[] s = { "one", "two", "three", };
for(int i = 0; i < s.length; i++)
System.out.println("s[" + i + "] = " + s[i]);
}
}
练习17:(2)创建一个类,它有一个接受一个 String参数的构造器。在构造阶段,打印该参数。创建一个该类的对象引用数组,但是不实际去创建对象赋值给该数组。当运行程序时,请注意来自对该构造器的调用中的初始化消息是否打印了出来。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 16:03
*/
class InitTest{
InitTest(String s){
System.out.println("InitTest()");
System.out.println(s);
}
}
public class InitTest17 {
public static void main(String[] args) {
InitTest[] it = new InitTest[10];
}
}
-没有输出-
练习18:(1)通过创建对象赋值给引用数组,从而完成前一个练习。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 16:03
*/
class InitTest{
InitTest(String s){
System.out.println("InitTest()");
System.out.println(s);
}
}
public class InitTest17 {
public static void main(String[] args) {
InitTest[] it = new InitTest[10];
for (int i = 0; i < it.length; i++) {
it[i] = new InitTest(Integer.toString(i));
}
}
}
InitTest()
0
InitTest()
1
InitTest()
2
InitTest()
3
InitTest()
4
InitTest()
5
InitTest()
6
InitTest()
7
InitTest()
8
InitTest()
9
可变参数列表
第二种形式提供了一种方便的语法来创建对象并调用方法,以获得与C的可变参数列表(C通常把它简称为 varargs
)一样的效果。这可以应用于参数个数或类型未知的场合。由于所有的类都直接或间接继承于 Object
类(随着本书的进展,读者会对此有更深入的认识),所以可以创建以 Object
数组为参数的方法,并像下面这样调用:
import com.sun.org.apache.xerces.internal.impl.dv.xs.DoubleDV;
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 16:10
*/
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","tow","three"});
printArray(new Object[]{new A(),new A(),new A()});
}
}
可以看到 print()
方法使用 Object
数组作为参数,然后使用 foreach
语法遍历数组,打印每个对象。标准Java库中的类能输出有意义的内容,但这里建立的类的对象,打印出的内容只是类的名称以及后面紧跟着的一个@
符号以及多个十六进制数字。于是,默认行为(如果没有定义toString()
方法的话,后面会讲这个方法的)就是打印类的名字和对象的地址。
你可能看到过像上面这样编写的 Java SE5之前的代码,它们可以产生可变的参数列表。然而,在 Java SE5中,这种盼望已久的特性终于添加了进来,因此你现在可以使用它们来定义可变参数列表了,就像在 printArray()
中看到的那样:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 16:18
*/
public class NewVarArgs {
static void printArray(Object...args){
for(Object o:args){
System.out.print(o +" ");
}
System.out.println();
}
public static void main(String[] args) {
printArray(new Object[]{
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());
printArray((Object[])new Integer[]{1,2,3,4});
printArray();
}
}
有了可变参数,就再也不用显式地编写数组语法了,当你指定参数时,编译器实际上会为你去填充数组。你获取的仍旧是一个数组,这就是为什么 print()
可以使用 foreach
来迭代该数组的原因。但是,这不仅仅只是从元素列表到数组的自动转换,请注意程序中倒数第二行,一个Integer
数组(通过使用自动包装而创建的)被转型为一个 Object
:数组(以便移除编译器警告信息),并且传递给了 printArray()
。很明显,编译器会发现它已经是一个数组了,所以不会在其上执行任何转换。因此,如果你有一组事物,可以把它们当作列表传递,而如果你已经有了个数组,该方法可以把它们当作可变参数列表来接受。
该程序的最后一行表明将0个参数传递给可变参数列表是可行的,当具有可选的尾随参数时这一特性就会很有用:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 16:25
*/
public class OptionalTrailingArgs {
static void f(int required,String... trailing){
System.out.print("required: "+required+" ");
for(String s:trailing){
System.out.print("s"+" ");
}
System.out.println();
}
public static void main(String[] args) {
f(1,"one");
f(2,"two","three");
f(0);
}
}
这个程序还展示了你可以如何使用具有 Object
之外类型的可变参数列表。这里所有的可变参数都必须是 String
对象。在可变参数列表中可以使用任何类型的参数,包括基本类型。下面的例子也展示了可变参数列表变为数组的情形,并且如果在该列表中没有任何元素,那么转变成的数据的尺寸为0:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 16:29
*/
public class VarargType {
static void f(Character... args){
System.out.println(args.getClass());
System.out.println("length "+args.length);
}
static void g(int... args){
System.out.println(args.getClass());
System.out.println("length "+args.length);
}
public static void main(String[] args) {
f('a');
f();
g(1);
g();
System.out.println("int[]: "+ new int[0].getClass());
}
}
class [Ljava.lang.Character;
length 1
class [Ljava.lang.Character;
length 0
class [I
length 1
class [I
length 0
int[]: class [I
getClass()
方法属于 Object
的一部分,我们将在第14章中做全面介绍。它将产生对象的类,并且在打印该类时,可以看到表示该类类型的编码字符串。前导的“[”
表示这是一个后面紧随的类型的数组,而紧随的"I"
表示基本类型int
。为了进行双重检査,我在最后一行创建了一个int数组,并打印了其类型。这样也就验证了使用可变参数列表不依赖于自动包装机制,而实际上使用的是基本类型.
然而,可变参数列表与自动包装机制可以和谐共处,例如:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 16:35
*/
public class AutoboxingVarargs {
public static void f(Integer... args){
for(Integer i:args){
System.out.print(i+" ");
}
System.out.println();
}
public static void main(String[] args) {
f(new Integer(1),new Integer(2));
f(4,5,6,7,8,9);
f(10,new Integer(11),12);
}
}
请注意,你可以在单一的参数列表中将类型混合在一起,而自动包装机制将有选择地将int
参数提升为 Integer
.
可变参数列表使得重载过程变得复杂了,尽管乍一看会显得足够安全:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 16:40
*/
public class OverloadingVarargs {
static void f(Character... args){
System.out.println("first");
for(Character c:args){
System.out.print(" "+c);
}
System.out.println();
}
static void f(Integer... args){
System.out.println("second");
for(Integer i:args){
System.out.print( " "+i);
}
System.out.println();
}
static void f(Long... args){
System.out.println("third");
}
public static void main(String[] args) {
f('a','b','c');
f(1);
f(2,1);
f(0);
f(0L);
}
}
first
a b c
second
1
second
2 1
second
0
third
在每一种情况中,编译器都会使用自动包装机制来匹配重载的方法,然后调用最明确匹配的方法。
但是在不使用参数调用f()
时,编译器就无法知道应该调用哪一个方法了。尽管这个错误可以弄清楚,但是它可能会使客户端程序员大感意外。
练习19:(2)写一个类,它接受一个可变参数的 String数组。验证你可以向该方法传递一个用逗号分隔的 String列表,或是一个 String[]
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 16:46
*/
public class VarargEx19 {
static void showString(String... args){
for(String s : args)
System.out.print(s + " ");
System.out.println();
}
public static void main(String[] args) {
showString("one", "TWO", "three", "four");
showString(new String[]{"1", "2", "3", "4"});
}
}
练习20:(1)创建一个使用可变参数列表而不是普通的main()语法的main()。打印所产生的args数组的所有元素,并用各种不同数量的命令行参数来测试它。
public class VarargEx20 {
public static void main(String... args) {
for(String s : args)
System.out.print(s + " ");
System.out.println();
}
}
枚举类型
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 16:52
*/
public enum Spiciness {
NOT,MILD,MEDIUM,HOT,FLAMING
}
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 16:51
*/
public class SimpleEnumUse {
public static void main(String[] args) {
Spiciness howHot = Spiciness.MEDIUM;
System.out.println(howHot);
}
}
这里创建了一个名为 Spiciness
的枚举类型,它具有5个具名值。由于枚举类型的实例是常量,因此按照命名馈例它们都用大写字母表示(如果在一个名字中有多个单词,用下划线将它们隔开)。
在你创建enum
时,編译器会自动添加一些有用的特性。例如,它会创建 toString()
方法,以便你可以很方便地显示某个enum
实例的名字,这正是上面的打印语句如何产生其输出的答案。编译器还会创建 ordinal()
方法,用来表示某个特定enum
常量的声明顺序,以及 static values()
方法,用来按照enum常量的声明顺序,产生由这些常量值构成的数组:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 16:57
*/
public class EnumOrder {
public static void main(String[] args) {
for(Spiciness s:Spiciness.values())
System.out.println(s +", ordinal "+s.ordinal());
}
}
尽管enum
看起来像是一种新的数据类型,但是这个关键字只是为enum
生成对应的类时,产生了某些编译器行为,因此在很大程度上,你可以将enum
当作其他任何类来处理。事实上,enum
确实是类,并且具有自己的方法。
enum
有一个特别实用的特性,即它可以在 switch
语句内使用:
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 17:02
*/
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.");
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();
}
}
由于 switch
是要在有限的可能值集合中进行选择,因此它与enum
正是绝佳的组合。请注意enum
的名字是如何能够倍加清楚地表明程序意欲何为的。
大体上,你可以将enum
用作另外一种创建数据类型的方式,然后直接将所得到的类型拿使用。这正是关键所在,因此你不必过多地考虑它们。在 Java SE5引进enum
之前,你必须花费大量的精力去保证与其等价的枚举类型是安全可用的。
练习21:(1)创建一个enum
,它包含纸币中最小面值的6种类型。通过 values()
循环并打印每个值及其 ordinal()
.
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 17:10
*/
public enum Bills {
ONE,FIVE,TEN,TWENTY,FIFTY,HUNDRED
}
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 17:11
*/
public class EnumEx21 {
public static void main(String[] args) {
for(Bills b:Bills.values()){
System.out.println(b + " ,ordinal "+b.ordinal());
}
}
}
练习22:(2)在前面的例子中,为enum
写ー个 switch
语句,对于每一个case
,输出该特定货币的描述。
/**
* @author Tangwenbo
* @version JDK 1.8
* @date 2021/8/1 17:13
*/
public class Wallet {
public static void main(String[] args) {
for(Bills b:Bills.values()){
System.out.println("Worth: ");
switch (b){
case ONE:
System.out.println("1元.");
break;
case FIVE:
System.out.println("5元.");
break;
case TEN:
System.out.println("10元.");
break;
case TWENTY:
System.out.println("20元.");
break;
case FIFTY:
System.out.println("50元.");
break;
case HUNDRED:
System.out.println("100元.");
break;
}
}
}
}
Worth:
1元.
Worth:
5元.
Worth:
10元.
Worth:
20元.
Worth:
50元.
Worth:
100元.