java学习笔记 java编程思想 第5章 初始化与清理
目录
- 5.1 用构造器确保初始化
- 练习1
- 练习2
- 5.2 方法重载
- 5.2.1 区分重载方法
- 5.2.2 涉及基本类型的重载
- 5.2.3 以返回值区分重载方法
- 5.3 默认构造器
- 练习3
- l练习4
- 练习5
- 练习6
- 练习7
- 5.4 this关键字
- 练习8
- 5.4.1 在构造器中调用构造器
- 练习9
- 5.4.2 static的含义
- 5.5 清理:终结处理和垃圾回收
- 5.5.1 finalize()的用途何在
- 5.5.2 你必须实施清理
- 5.5.3 终结条件(The termination condition)
- 练习10
- 练习11
- 练习12
- 5.5.4 垃圾回收器如何工作
- 5.6 成员初始化
- 5.6.1 指定初始化
- 5.7 构造器初始化
- 5.7.1 初始化顺序
- 5.7.2 静态数据的初始化
- 5.7.3 显示的静态初始化
- 练习13
- 练习14
- 5.7.4 非静态实例初始化
- 练习15
- 5.8 数组初始化
- 练习16
- 练习17
- 练习18
- 5.8.1 可变参数列表
- 练习19
- 练习20
- 5.9 枚举类型
- 练习21
- 练习22
C++引入构造器(constructor)概念,是一个在创建对象时被自动调用的特殊方法。java也采用了构造器,并额外添加了“垃圾回收期”。
5.1 用构造器确保初始化
java和C++一样采用的相同的初始化方案:构造器的名称必须和类的名称相同。在创建对象时(new Rocl();
),将会为对象分配存储空间,冰火调用相应的构造器,调用构造器就可以完成适当的初始化了。不接受任何参数的构造器叫做**默认构造器或者无参构造器**。和其他方法一样,构造器也有带形参的。
从概念上说,“初始化”和创建应该是相互独立的,但是在代码中是没有对initalize()
方法的明确调用,在java中,“初始化”和“创建”捆绑在一起,二者不可分离。
f构造器和返回void方法的区别:
- 构造器是一种特殊的方法,它没有返回值,与返回值为boid的方法不同。构造器不会返回任何东西,只能通过new确实返回新创建的对象的引用,但是构造器本身没有任何返回值。如果构造器有返回值,那么编译器就需要处理不同类型的返回值。
- 对于返回void的方法,虽然方法本身不会return,但仍然可以让它返回别的东西。
练习1
public class Exec01 {public static void main(String[] args) {A a = new A();System.out.println(a.s); // null}
}class A {String s;
}
练习2
public class Exec02 {public static void main(String[] args) {B b = new B("hello");System.out.println(b.s);}
}class B {String s;B() {System.out.println("无参数构造调用...");}B(String s) {System.out.println("有参数构造调用...");this.s = s;}
}
通过有参数构造器可以,传入自定义的初始值,对对象初始化。
5.2 方法重载
C中没有方法重载,每个函数都有依噶唯一的名称。
java和C++中,构造器是强制重载方法名的另一个原因。构造器的名字由类名决定,所以只能有一个构造器名。但是如果想用多种方式创建对象怎么办?这是为了让方法名相同形参不同的构造器存在,必须使用方法重载。同时其他方法也可以方法重载。
5.2.1 区分重载方法
如果几个方法的名字相同,编译器如何知道你调用了哪个方法呢?每一个重载的方法都必须有一个独一无二的形参列表。编译器通过重载方法的形参列表来区分。甚至,形参的顺序不同,也可以区分两个方法。但是这样做会使代码难以维护。
public class Test01 {public static void main(String[] args) {f();f(1, "asdf");f("asdf", 1);}static void f() {System.out.println("111111");}static void f(int a, String s) {System.out.println("222222");}static void f(String s, int a) {System.out.println("333333");}/*这两个方法是相同的。形参列表只区分形参的数据类型,不区分形参的名字。static void f(int a, int b) {System.out.println("111111");}static void f(int b, int a) {System.out.println("111111");}*/
}
// 运行结果
111111
222222
333333
5.2.2 涉及基本类型的重载
基本类型能从“较小”的数据类型,自动提升为“较大”的数据类型,这个过程如果涉及到重载,可能会造成一些混淆。
如果方法传入的实参的数据类型小于方法中声明的形参的数据类型,实参的数据类型就会被自动提升。char
类型不一样,如果无法找到接收char
的重载方法,就会将char
提升为int
类型。
public class Test02 {public static void main(String[] args) {f1(10);f1(10.0);}static void f1(char x) {System.out.print("f1(char) ");}static void f1(byte x) {System.out.print("f1(byte) ");}static void f1(short x) {System.out.print("f1(short) ");}static void f1(int x) {System.out.print("f1(int) ");}static void f1(long x) {System.out.print("f1(long) ");}static void f1(float x) {System.out.print("f1(float) ");}static void f1(double x) {System.out.println("f1(double)");}
}// 运行结果
f1(int) f1(double)
常数值10会被当做int类型处理,只要有f1(int)
就会被调用。如果将f1(int)
方法注释掉,那么会调用f1(long)
。如果f1(int)
和f1(llong)
都被注释了,就调用f1(float)
。接着注释掉f1(float)
,会调用f1(double)
。接着注释掉f1(double)
,就会无法编译通过,编译器提示如下错误:
如果传入的实参数据类型大于重载方法中声明的形参的数据类型,就需要将实参做narrow conversion,如果不做,编译器就会报错。
Error:(6, 9) java: 对于f1(int), 找不到合适的方法方法 com.qww.Test02.f1(char)不适用(参数不匹配; 从int转换到char可能会有损失)方法 com.qww.Test02.f1(byte)不适用(参数不匹配; 从int转换到byte可能会有损失)方法 com.qww.Test02.f1(short)不适用(参数不匹配; 从int转换到short可能会有损失)
这时,可以将10转换为其他类型,才能通过编译。
// 改为
f1((byte) 10);
// 运行结果
f1(byte)
5.2.3 以返回值区分重载方法
void f() {}
int f() {return -1}
通过f();
调用方法,java没法通过返回值判断调用的是哪个f()
,所以根据方法的返回值来区分重载方法是行不通的。
5.3 默认构造器
如果在类里面,没有定义任何构造器,那么编译器会在编译时为该类自动创建一个无参构造器。
如果已经在类里面定义了构造器(无论是有参还是无参),编译器就不会提供构造器了。
如果在类里面,没有定义任何构造器,那么编译器会在编译时为该类自动创建一个无参构造器。
如果已经在类里面定义了构造器(无论是有参还是无参),编译器就不会提供构造器了。
练习3
public class Exec03 {public static void main(String[] args) {new C();}
}
class C {C() {System.out.println("C()");}
}
// 运行结果
C()
l练习4
public class Exec04 {public static void main(String[] args) {new D("hello");}
}class D {D() {System.out.println("D()");}D(String s) {System.out.println("D(" + s + ")");}
}
// 运行结果
D(hello)
练习5
public class Exec05 {public static void main(String[] args) {Dog d = new Dog();d.bark(1);d.bark("");d.bark(true);}
}class Dog {void bark(int x) {System.out.println("barking");}void bark(String x) {System.out.println("howling");}void bark(boolean x) {System.out.println("hanhan");}
}// 运行结果
barking
howling
hanhan
练习6
public class Exec06 {public static void main(String[] args) {Dog d = new Dog();d.bark(1, "");d.bark("", 1);}
}class Dog {void bark(int x, String s) {System.out.println("barking");}void bark(String s, int x) {System.out.println("howling");}}
// 运行结果
barking
howling
练习7
public class Exec07 {public static void main(String[] args) {new A();}
}class A {}
# 执行javap -c -v com.qww.exec07.Exec07
Classfile /E:/qiweiwei/code/java/thinking-in-java/out/production/chapter05/com/qww/exec07/Exec07.classLast modified 2021-4-3; size 430 bytesMD5 checksum 53fdb1829eb6d6282d5a81860cfe9843Compiled from "Exec07.java"
public class com.qww.exec07.Exec07minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref #5.#19 // java/lang/Object."<init>":()V#2 = Class #20 // com/qww/exec07/A#3 = Methodref #2.#19 // com/qww/exec07/A."<init>":()V#4 = Class #21 // com/qww/exec07/Exec07#5 = Class #22 // java/lang/Object#6 = Utf8 <init>#7 = Utf8 ()V#8 = Utf8 Code#9 = Utf8 LineNumberTable#10 = Utf8 LocalVariableTable#11 = Utf8 this#12 = Utf8 Lcom/qww/exec07/Exec07;#13 = Utf8 main#14 = Utf8 ([Ljava/lang/String;)V#15 = Utf8 args#16 = Utf8 [Ljava/lang/String;#17 = Utf8 SourceFile#18 = Utf8 Exec07.java#19 = NameAndType #6:#7 // "<init>":()V#20 = Utf8 com/qww/exec07/A#21 = Utf8 com/qww/exec07/Exec07#22 = Utf8 java/lang/Object
{public com.qww.exec07.Exec07();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 3: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcom/qww/exec07/Exec07;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: new #2 // class com/qww/exec07/A3: dup4: invokespecial #3 // Method com/qww/exec07/A."<init>":()V7: pop8: returnLineNumberTable:line 6: 0line 7: 8LocalVariableTable:Start Length Slot Name Signature0 9 0 args [Ljava/lang/String;
}
SourceFile: "Exec07.java"
看不懂上面这些反编译的代码,以后再来补上,嘻嘻。
先用jadx-gui-1.2.0软件反编译一下吧:
发现在类A
中有无参构造器。
5.4 this关键字
有两个同样类型的对象a和b,但是只有一个方法peel(),编译器是如何知道peel()方法是被a调用还是被b调用的呢?
class BananaPeel {void peel(int x) {}
}public class Test03 {public static void main(String[] args) {BananaPeel a = new BananaPeel();BananaPeel b = new BananaPeel();a.peel(1);b.peel(2);}
}
为了能用简便、面向对象的语法编写代码,即“发送消息给对象”,编译器在幕后做了一些工作。编译器把“所操作的对象引用”作为第一个参数传递给peel();。所以两个方法就变成了:
BananaPeel.peel(a, 1);
BananaPeel.peel(b, 1);
这与反射中调用方法的invoke
有些类似invoke(Object obj, Object... args)
。
如果想在方法内部使用对当前对象的引用,就可以使用this
关键字。因为这个引用时编译器“偷偷”传入的,没有标识符可用。
this
关键字只能在方法的内部使用,表示对“调用方法的那个对象”的引用。如果在方法内部调用本类中的其他方法,没必要使用this
,之间调研就行,编译器会自动添加this
。
只有在需要明确指出对当前对象的引用时,才需要使用this
。例如:当需要返回对当前对象的引用时,就常常在return语句里使用this
。不要想着随意添加上this
,会“更加清楚明确”。
public class :eaf {int i = 0;Leaf increment() {i++;return this;}
}
这样的写法,我以前经常在链式调用的代码中见到过。
this
关键字对于将当前对象传递给其他方法也很有用:
public class Test05 {public static void main(String[] args) {new Person().eat(new Apple());}
}class Person {void eat(Apple apple) {apple = apple.getPeeled();System.out.println("asdfghjkl;");}
}class Peeler {static Apple peel(Apple apple) {// ...return apple;}
}/*** <p>Apple需要调用Peeler.peel()方法, 它是一个外部的工具方法,将执行由于某种原因而必须放在Apple外部的操作* (也许因为该外部方法要应用于许多不同的类,而你却不想重复这些代码)。* 为了将其自身传递该外部方法,Apple必须使用<code>this</code>关键字。</p>*/
class Apple {Apple getPeeled() {return Peeler.peel(this);}
}
// 运行结果
asdfghjkl;
练习8
public class Exec08 {public static void main(String[] args) {A a = new A();a.method1();}
}class A {void method1() {method2();this.method2();}void method2() {System.out.println("method2被调用了...");}
}// 运行结果
method2被调用了...
method2被调用了...
5.4.1 在构造器中调用构造器
我们可以在一个类里面,编写多个构造器,也就是多个构造器重载。但是编写多个构造器时,会有很多初始化的重复性代码。可以使用this
关键字,通过在一个构造器里调用另一个构造器的方法,来实现这个,
通常this
指的是“这个对象”或者“当前对象”,而且它本身表示对当前对象的引用。在构造器中,为this
添加了参数列表,就和=有了不同的含义,是调用符合参数类别的其他构造器。除构造器之外,其他任何方法都不能调用构造器,比如在一个实例方法体中使用this(实参),这是错误的。
public class Test06 {public static void main(String[] args) {System.out.println(new A());System.out.println(new A(1));System.out.println(new A(2, "hello", 300.0));System.out.println(new B(1, "hello"));}
}
class A {int x;String s;double y;A() {this(0, "", 0.0);}A(int x) {this(x, "", 0.0);}A(int x, String s, double y) {this.x = x;this.s = s;this.y = y;}public String toString() {return "x=" + x + ", s=" + s + ", y=" + y;}
}class B {int x;String s;B() {x = 0;s = "";}B(int x) {this("");this.x = x;}B(String s) {this.s = s;}B(int x, String s) {this(x);// this(s); // Error:(57, 13) java: 对this的调用必须是构造器中的第一个语句this.s = s;}public String toString() {return "x=" + x + ", s=" + s;}
}
// 运行结果
x=0, s=, y=0.0
x=1, s=, y=0.0
x=2, s=hello, y=300.0
x=1, s=hello
构造器中调用另一个构造器时,this
调用语句,只能在构造器中的第一条语句。
练习9
public class Exec09 {public static void main(String[] args) {System.out.println(new A());System.out.println(new A(1000, "jack"));}
}class A {int x;String s;A() {this(1, "unknown");}A(int x, String s) {this.x = x;this.s = s;}public String toString() {return x + ", " + s;}
}
// 运行结果
1, unknown
1000, jack
5.4.2 static的含义
static
方法就是没有this
的方法。在static
方法内部中不能调用非静态方法,反过来在非静态方法中是可调用静态方法的。
可以做不创建对象,通过类本身调用静态方法。
// 可以给静态方法传入一个对象的引用,这样可以实现惊天方法调用非静态方法。
public class Test07 {public static void main(String[] args) {A.method2(new A());}}class A {void method1() {System.out.println("非静态方法被调用了。。。");}static void method2(A a) {a.method1();}
}
// 运行结果
非静态方法被调用了。。。
5.5 清理:终结处理和垃圾回收
java垃圾回收器,只知道释放那些由new创建的对象,在没有任何引用指向它后,会在释放掉该对象所占的内存空间。
但是也有用特殊情况,如果这个对象不是通过new
创建的对象,那么垃圾回收器就不知道该如何释放了。为了解决这个情况,java在object类中提供了一个方法**finalize()
**方法,垃圾回收器在释放对象之前会调用该方法。这是java为我们提供的释放对象的时刻(垃圾回收时刻),我们可以在finalize()
方法里编写一些清理工作的代码。
一些C++程序员会误认为java中的finalize()
方法是C++中的析构函数(C++中销毁对象必须用到这个函数)。区别是:C++中,对象一定会被销毁(如果程序中没有缺陷);在java中对象不一定总是被垃圾回收。也就是说:
- 对象可能不被垃圾回收。
- 垃圾回收并不等于“析构”。
5.5.1 finalize()的用途何在
- 垃圾回收只与内存相关。
使用垃圾回收器的原因是为了回收程序不再使用的内存。垃圾回收器负责释放对象占据的所有内存。finalize()方法针对特使情况,不是通过new创建的对象分配的内存空间。为什么会这种特殊情况创建的对象呢?
是因为在分配内存时,java可能调用C/C++中的“本地方法”创建的对象,而非java代码通过new来创建的。这种情况可能是使用malloc()
函数创建的分配非内存空间,除非调用free()
函数,否则存储空间将无法释放,这会造成内存泄漏。
释放内存的方法是:因为free()
方法是C/C+=中的函数,所以在finalize()
中用本地方法来调用free()
。
所以不要过多使用finalize()方法、
5.5.2 你必须实施清理
java不允许创建局部对象,必须使用new关键字创建对象。Java没有析构函数,因为垃圾回收器会帮助释放空间。但是垃圾回收器不等于析构函数。绝对不能直接调用finalize()方法。如果想要进行除了释放存储空间之外的清理工作,那就需要明确调用某个恰当的Java方法,这样就等同于析构函数了。
无论是garbage collection还是finalization,都不保证一定会发生。如果jvm没有面临内存耗尽的情况,它是不会去浪得时间执行垃圾回收来回顾内存的。
5.5.3 终结条件(The termination condition)
通常,不要使用finalize()
方法,我们必须独立创建并明确调用其他的“清理”方法、似乎finalize()
方法对我们来说是只有对永远不会使用的模糊清理内存有用了。此外,finalize()
可以验证是否对象所占内存开始被释放。
public class Test07 {public static void main(String[] args) {Book book = new Book(true);book.checkIn();new Book(true);// 强制进行gc回收动作System.gc();}
}class Book {boolean checkedOut = false;Book() { }Book(boolean checkOut) {checkedOut = checkOut;}/*使用f<code>finalize()</code>方法去检测对象是否真确被清理。*/@Overrideprotected void finalize() throws Throwable {if (checkedOut) {System.out.println("Error : checked out.");}// super.finalize();}void checkIn() {checkedOut = false;}
}// 运行结果
Error : checked out.
System.gc();
用于强制进行终结动作(finalization)。
练习10
public class Exec10 {public static void main(String[] args) {for (int i = 0; i < 10000; i++) {new A();}System.gc();}
}class A {int x = 10;double a = 10.0;Object[] objs = {"", "", "", "1", "a"};@Overrideprotected void finalize() throws Throwable {System.out.println("garbage collection.......");}
}
// 运行结果
garbage collection.......
garbage collection.......
garbage collection.......
garbage collection.......
garbage collection.......
garbage collection.......
练习11
/*** 当垃圾回收器找到一个有资格进行回收但有一个对象的对象时,finalizer它不会立即取消分配它。* 垃圾回收器试图尽快完成,因此它只是将对象添加到具有待定finalizer的对象列表中。finalizer稍后在单独的线程上调用。* 通过System.runFinalization在垃圾回收之后调用该方法,可以告诉系统立即尝试运行挂起的finalizer。* 但是,如果要强制运行finalizer,则必须自己调用它。* 垃圾回收器不保证将回收任何对象或将调用finalizer。这只是“尽力而为”。但是,很少需要强制finalizer以实际代码运行。*/
public class Exec11 {public static void main(String[] args) {WebBank bank1 = new WebBank(true);WebBank bank2 = new WebBank(true);new WebBank(true);// Proper cleanup: log out of bank1 before going home:bank1.logOut();// Forget to logout of bank2 and unnamed new bank// Attempts to finalize any missed banks: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: ");// using deprecated since 1.1 method:System.runFinalizersOnExit(true);}
}// initialization/BankTest.java
// TIJ4 Chapter Initialization, Exercise 11, page 177
// Modify the previous exercise so that finalize() will always be called.
class WebBank {boolean loggedIn = false;WebBank(boolean logStatus) {loggedIn = logStatus;}void logOut() {loggedIn = false;}protected void finalize() {if(loggedIn)System.out.println("Error: still logged in");// Normally, you'll also call the base-class version:// super.finalize();}
}
// 运行结果
Try 1:
Try 2:
Try 3:
Try 4:
Error: still logged in
Error: still logged in
练习12
public class Exec12 {public static void main(String[] args) {Tank tank1 = new Tank();tank1.clean();System.gc();System.out.println("==============");new Tank();// 忘记清理数据,也就是忘记调用clean()System.gc();System.runFinalization();}
}class Tank {// 状态 true 表示满的,false表示空的boolean isEmpty = true;Tank() {isEmpty = false;}void clean() {System.out.println("clean up...");isEmpty = true;System.out.println("status is empty.");}@Overrideprotected void finalize() throws Throwable {if (!isEmpty) {System.out.println("Error: tank is full! You must clean data by call clean().");}}
}// 运行结果
clean up...
status is empty.
==============
Error: tank is full! You must clean data by call clean().
5.5.4 垃圾回收器如何工作
对于其他语言,在堆上分配对象的代价是非常高昂的。垃圾回收器可以提高在堆上对象的创建速度。
存储空间的释放会影响存储空间的分配,使得Java在堆上分配空间的速度,可以和其他语言在堆栈上分配空间的速度相媲美。
对于其他语言,在堆上分配对象的代价是非常高昂的。垃圾回收器可以提高在堆上对象的创建速度。存储空间的释放会影响存储空间的分配,使得Java在堆上分配空间的速度,可以和其他语言在堆栈上分配空间的速度相媲美。
垃圾回收器对对象重新排列,实现了一种高效、有无限空间的可供分配的堆模型:Java的堆并不完全像传送带一样工作,因为像传送带一样的工作的话,会频繁的进行内存页面的调度,这会显著影响性能,最终导致内存耗尽。得益于垃圾回收器的介入,垃圾回收器一边回收空间,一边使堆中的对象紧凑排列,这使得“堆指针”更容易移动到更靠近传送带的开始处,也就避免了页面错误。
了解引用计数:引用计数是一种简单但速度很慢的垃圾回收技术。每个对象收含有一个引用计数器,当有引用指向该对象时,引用计数加1.当引用理该对象的作用域或者为null
时,该对象的引用计数减1。虽然管理引用计数得 列表开销不大,但是这笔开销会在正工程序的生命周期中都在。垃圾回收器会遍历含有全部对象引用计数的列表,当发现某个对象引用计数为0时,就立即释放该对象所占用的空间。这种方法的缺点是,如果对象之间存在循环引用,可能会出现“对象应该被回收,但是引用计数不是0”的情况。对于垃圾回收器来说,定位这中交互引用的对象组需要的工作量极大。引用计数法常用来说明垃圾回收的工作方法,但从未被应用在任何一种jvm的实现中。
更快的垃圾回收器不采用引用计数法。对于任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区中的引用。这个引用链条可能会穿过很多个对象层次。因此,如果从堆栈和静态存储区开始,遍历所有的引用,就能找到所有“活”的对象。对于发现的每个引用,必须追踪它所引用的对象,然后是该对象包含的所有引用,如此反复,直到“根源于堆栈和静态存储区的引用”所形成的网络全部访问为止。注意,访问的过的对象必须是“活”的。这就解决了“交互自引用的对象组”问题,这种现象根本不会被发现,因为也会被自动回收了。
jvm采用一种自适应的垃圾回收技术。不同jvm对于处理找到的存活对象的方式不同。其中一种方式是停止-复制(stop-and-copy):先暂停小衡虚的运行(不属于后台回收模式),然后将所有存活的对象从当前的堆中复制带另一个堆中,没有复制的全部都是垃圾,会被回收。当对象被复制到新堆中时,已经是保持紧凑排列了,然后就可以分配新空间了。把对象从一个堆复制到另一个堆中,需要修改指向它的所有引用。在堆活静态存储区的引用可以直接修改,但可能会有指向对象的其他引用,这些引用只有在遍历过程中才能找到(可以想象成有一个表格,它将旧地址银蛇到新地址)。
这种复制式的垃圾回收器,效率很低。原因有两点。
- 第一:先得有两个堆用来复制对象,这就需要维护比实际需要多一倍的空间。
- 第二:复制。程序进入稳定状体之后,可能会产生少量垃圾,甚至没有垃圾产生。但是复制式垃圾回收器仍然要将多有内存复制一份到别处,这很浪费空间。解决方法是:jvm进行检查。如果没有新垃圾产生,就转换到自适应的模式(这个魔术速度快)。一般的标记-清理方法速度很慢。
标记-清理(Mark-and-sweep)、停止-复制(Stop-and-copy)
当程序稳定,很少产生垃圾时,jvm会切换到标记-清理方式。当堆空间产生很多碎片时,jvm会切换回停止-复制方法
JIT(Just-In-Time)即时编译技术。可以把程序全部或部分翻译成本地机器码(这本来是jvm的活),从而提高程序运行速度。
5.6 成员初始化
对于方法的局部变量,如果没有初始化,编译器会报错。
如果是类的成员变量,系统会初始化为默认值。
5.6.1 指定初始化
在定义变量处。该它赋值。赋值方式,可以是常量,可以是new出来的对象,也可以是方法。
5.7 构造器初始化
可以在对象创建时,通过构造器对变量初始化。注意,我们无法阻止系统自动为变量赋默认值的这个操作,因为这个操作在构造器执行之前就已经完成了。
public class Test08 {int i;Test08() {i = 10;}
}
首先变量i被赋值为int
类型的默认值0,然后再创建对象时,i会变成10,
5.7.1 初始化顺序
在类里面,变量定义的先后顺序,决定了它们的初始化顺序。即使变量定义在多个方法之间,成员变量同样会在所有方法(包括构造器)之前进行初始化、
public class Test09 {public static void main(String[] args) {new A().f1();}
}class A {void f1() {System.out.println("f1()");}B b1 = new B(1);void A() {B b2 = new B(2);}B b2 = new B(3);
}class B {B() { }B(int i) {System.out.println("B : " + i);}
}
// 运行结果
B : 1
B : 3
B : 2
f1()
b2会被初始化两次,第一次在调用构造方法之前,第二次在调用构造方法时(第一次初始化引用的对象将会被就丢弃)。
5.7.2 静态数据的初始化
不管创建多少个对象,static数据只有一份。static关键字不能用在局部变量上,只能用在成员变量上。在定义处和非静态数据一样,系统也会初始化默认值。
class Bowl {Bowl() {}Bowl(int marker) {System.out.printf("Bowl(%d)\n", marker);}void f1(int marker) {System.out.printf("f1(%d)\n", marker);}
}class Table {static Bowl b1 = new Bowl(1);Table() {System.out.println("Table()");b1.f1(1);}void f2(int marker) {System.out.printf("f2(%d)\n", marker);}static Bowl b2 = new Bowl(2);
}class Cupboard {Bowl b3 = new Bowl(3);static Bowl b4 = new Bowl(4);Cupboard() {System.out.println("Cupboard()");b4.f1(2);}static Bowl b5 = new Bowl(5);
}public class Test10 {public static void main(String[] args) {System.out.println("main()");new Cupboard();}static Table tbl = new Table();static Cupboard cupbd = new Cupboard();
}
// 运行结果
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f(2)
main()
Bowl(3)
Cupboard()
f(2)
当第一次调用构造器创建对象,或者访问类中的静态方法/静态变量时,编译器首先去classpath找对应类的
.class
字节码文件,然后载入该字节码,创建对应的Class
对象。在加载字节码的过程中,会执行静态变量的初始化操作和静态代码块。执行顺序是从上到下。
当对象创建成功时,会在堆上为该对象分配一块存储空间,实例变量这时会赋值为默认值。然后执行实例变量定义处的初始化操作和执行普通代码块。执行顺序从上到下。
然后执行构造器,或者静态方法/静态变量。
5.7.3 显示的静态初始化
多个静态初始化操作,放到一块用花括号括起来,叫做静态代码块。
静态代码块和静态变量一样,在类加载时执行,并且只执行一次(当第一次调用构造器,或者第一次访问静态变量/调用静态方法时)。
练习13
public class Exec13 {public static void main(String[] args) {System.out.println("main()");// Cups.cup1.f(99); // (1)}static Cups cups1 = new Cups(); // (2)static Cups cups2 = new Cups(); // (2)
}class Cup {Cup(int marker) {System.out.println("Cup("+ marker +")");}void f(int x) {System.out.println("f((" + x + ")");}
}class Cups {static Cup cup1;static Cup cup2;static {cup1 = new Cup(1);cup2 = new Cup(2);}Cups() {System.out.println("Cups()");}
}
// 运行结果
main()
Cup(1)
Cup(2)
f(99)
// 将(1)注释掉,运行(2)的结果,静态代码块的初始化操作只执行了一次
Cup(1)
Cup(2)
Cups()
Cups()
main()
练习14
public class Exec14 {static class A {static String s1 = "asdf";static String s2;static {s2 = "jkl;";}static void f() {System.out.println(s1);System.out.println(s2);}}public static void main(String[] args) {A.f();}
}
5.7.4 非静态实例初始化
实例变量初始化代码块:
{mug1 = new Mug(1);mug2 = new Mug(2);print("mug1 & mug2 initialized");
}
普通代码块和静态代码块基本上一模一样,只是少了static关键字。对于“匿名内部类”的初始化,这种语法是必须的。到那时它也使我们无论调用哪个显示构造器,普通代码块都会被执行。普通代码块在构造器之前执行。
练习15
public class Exec15 {String s;{s = "asdf";}public static void main(String[] args) {System.out.println(new Exec15().s);}
}
// 运行结果
asdf
5.8 数组初始化
// 方法1
int[] a1 = {1, 2, 3};
// 方法2
int[] a2 = new int[3];
a[0] = 1;
a[1] = 2;
a[2] = 3;
如果访问的数组下标不在[0, length-1],运行程序时,编译器就抛出java.lang.ArrayIndexOutOfBoundsException
异常。
练习16
public class Exec16 {public static void main(String[] args) {String[] arr = {"a", "s", "d", "f"};for (int i = 0; i < arr.length; i++) {System.out.println(arr[i]);}}
}
// 运行结果
a
s
d
f
练习17
public class Exec17 {public static void main(String[] args) {A[] arr;}
}class A {A() { }A(String s) {System.out.println(s);}
}
练习18
public class Exec18 {public static void main(String[] args) {A[] arr;arr = new A[2]{new A("a"), new A("b")};}
}class A {A() { }A(String s) {System.out.println(s);}
}
// 运行结果
a
b
5.8.1 可变参数列表
传入的参数个数和类型未知时,可以将方法的形参列表修改为一个Object[]
数组的形式来实现。
public class Test16 {public static void main(String[] args) {printArr(new Object[]{1, 2, 'a', 4, 5, });printArr(new Object[]{"as", "df", 1.0});printArr(new Object[]{ 1, "3", new A(),});}public static void printArr(Object[] objs) {for (Object obj : objs) {System.out.print(obj + " ");}System.out.println();}
}class A { }
// 运行结果
1 2 a 4 5
as df 1.0
1 3 com.qww.test16.A@1540e19d
在Javase5中,加入了可变长参数列表新特性。不用显式地编写数组了,其实编译器实际上会将我们传入的参数封装为一个数组(可以使用foreach来遍历它)。
public class Test17 {public static void main(String[] args) {// 可以传入一个Object数组printArr(new Object[]{1, 2, 'a', 4, 5, });printArr(new Object[]{"as", "df", 1.0, });printArr(new Object[]{ 1, "3", new A(), });// 也可以传入多个实参printArr(1, 2, 'a', new A());// 不传入参数也是可行的printArr();}public static void printArr(Object... args) {for (Object obj : args) {System.out.print(obj + " ");}System.out.println();}
}class A { }// 运行结果
1 2 a 4 5
as df 1.0
1 3 com.qww.test17.A@1540e19d
1 2 a com.qww.test17.A@677327b6
也可以在形参列表的末尾添加可变参数列表,但是可变参数列表不能位于形参列表的第一个位置上:
public class Test18 {public static void main(String[] args) {// 可变参数列表必须是String类型,args数组的长度为2printArr(1, "asdf", "jkl;");// 也可以不传入可变长参数,args数组的长度为0printArr(111);}public static void printArr(int a, String... args) {System.out.println("a=" + a);for (Object obj : args) {System.out.print(obj + " ");}System.out.println();System.out.println("length: " + args.length);}/*// Vararg parameter must be the last in the listpublic static void f(String... args, int a) {}*/
}
// 运行结果
a=1
asdf jkl;
length: 2
a=111length: 0
可变长参数列表中,自动装箱机制
public class Test19 {public static void main(String[] args) {f(new Integer(1), new Integer(1), new Integer(1));f(2, 2, 2);// 可以在单一的参数列表中将基本类型和包装类型混在一块,回有选择地将`int`类型包装成为`Integer`类型f(3, new Integer(3), 3);}static void f(Integer... args) {for (Integer i : args) {System.out.print(i + " ");}System.out.println();}
}
// 运行结果
1 1 1
2 2 2
3 3 3
可变参数列表使得方法重载变得复杂了。在只传入100一个参数时会出现问题,分不清该调用哪个方法了。
解决方法就是,将重载方法的非可变长参数修改为不同的类型。
public class Test20 {public static void main(String[] args) {f(100, 1, 2);f(100, "asdf", "ghjk");f(100, 1L, 10L);f(100, 1.0, 2.0);// Ambiguous method call// f(100);}static void f(int a, Integer... args) { }static void f(int a, String... args) { }static void f(int a, Long... args) { }static void f(int a, Double... args) { }
}
练习19
public class Exec19 {public static void main(String[] args) {f("as", "df");f(new String[]{"as", "df"});}static void f(String... args) {for (String arg : args) {System.out.print(arg);}System.out.println();}
}
// 运行结果
asdf
asdf
练习20
public class Exec20 {public static void main(String... args) {for (String arg : args) {System.out.print(arg + " ");}System.out.println();}
}
// 传入的命令行参数
1 20.0 hello 大风起自云飞扬 \"
// 运行结果
1 20.0 hello 大风起自云飞扬 "
5.9 枚举类型
javase5添加了enum
关键字,并且它的功能比C/C+=中的枚举要完备的多。
/*** 创建一个Spiciness枚举类型* 它具有5个具体值 NOT, MILD, MEDIUM, HOT, FLAMING* 因为枚举类型的实例是常量,所以按照命名惯例都用大写字母表示,多个单词用下划线隔开。*/
public enum Spiciness {NOT, MILD, MEDIUM, HOT, FLAMING
}public class Test21 {public static void main(String[] args) {// 为了使用enum,需要创建一个该枚举类型的引用,将它赋值给某个实例。Spiciness howHot = Spiciness.MEDIUM;System.out.println(howHot);}
}
在创建enum
时,编译器会自动添加一些有用的特性。
toString()
方法:用来显示enum
实例的名字;ordinal()
方法:用来表示某个特定enum
常量的声明顺序;static values()
方法:用来按照enum
常量的声明顺序,产生有这些常量值构成的数组。
public class Test21 {public static void main(String[] args) {for (Spiciness s : Spiciness.values()) {System.out.println(s + ", ordinal=" + s.ordinal());}}
}
// 运行结果
NOT, ordinal=0
MILD, ordinal=1
MEDIUM, ordinal=2
HOT, ordinal=3
FLAMING, ordinal=4
enum
不是新的数据类型,其实是类,enum
关键字只是规定了编译器的某些行为。 枚举放到switch
语句上。
import java.util.Random;public class Test22 {static Spiciness degree;public static void main(String[] args) {Spiciness[] arr = Spiciness.values();Random r = new Random();int index = r.nextInt(arr.length);degree = arr[index];switch (degree) {case NOT:System.out.println("NOT"); break;case MILD:System.out.println("MILD"); break;case MEDIUM:System.out.println("MEDIUM"); break;case FLAMING:System.out.println("FLAMING"); break;case HOT:System.out.println("HOT"); break;}}
}
练习21
public class Exec21 {public static void main(String[] args) {Currency[] arr = Currency.values();for (Currency c : arr) {System.out.print(c + " ");}System.out.println();}}enum Currency {ONE, FIVE, TEN, TWENTY, FIFTY, ONE_HUNDRED;
}// 运行结果ONE FIVE TEN TWENTY FIFTY ONE_HUNDRED
练习22
public class Exec22 {public static void main(String[] args) {Currency c = Currency.FIFTY;switch (c) {case ONE:System.out.println("1"); break;case FIVE:System.out.println("2"); break;case TEN:System.out.println("10"); break;case TWENTY:System.out.println("20"); break;case FIFTY:System.out.println("50"); break;default:System.out.println("100"); break;}}}enum Currency {ONE, FIVE, TEN, TWENTY, FIFTY, ONE_HUNDRED;void f(int a) {System.out.println(a);}
}
// 运行结果
50
java学习笔记 java编程思想 第5章 初始化与清理相关推荐
- java编程思想初始化引用,JAVA编程思想--第5章 初始化与清理
随着计算机革命的发展,"不安全"的编程方式已逐渐成为编程代价高昂的主因之一. 初始化和清理(cleanup)是涉及安全的两个问题.初始化时,忘记初始化时许多错误的来源,还有就是不知 ...
- java学习笔记 java编程思想 第6章 访问权限控制
文章目录 6.1 包:库单元(the library unit) 6.1.1 代码组织 6.1.2 创建独一无二的包名 练习1 练习2 6.1.3 定制工具类 6.1.4 用import改变行为 练习 ...
- Java学习笔记-网络编程
Java提供了网络编程,并且在实际中有着大量运用 网络编程 网络编程概述 网络模型 OSI参考模型 TCP/IP参考模型 网络通讯要素 IP地址 端口号 传输协议 网络参考模型 网络通讯要素 IP地址 ...
- java学习笔记—java的学习路线
Java体系涉及到三个方面:J2SE,J2EE,J2ME(KJAVA). J2SE,Java 2 Platform Standard Edition,我们经常说到的JDK,就主要指的这个,它是三者的基 ...
- 狂神说Java学习笔记 Java基础
目录 机器语言 第二代语言(汇编语言) 第三代语言 高级语言 Java特性和优势 JDK(Java Development Kit) JRE(Java Runtime Enviroment) JVM( ...
- Java学习笔记--Java中必记常见异常
JAVA常见异常 Java.io.NullPointerException null 空的,不存在的 NullPointer 空指针 空指针异常,该异常出现在我们操作某个对象的属性或方法时,如果该对象 ...
- 一起读Java编程思想(2)---构造器的初始化与清理
初始化与清理 用构造器确保初始化 每个类都要定义一个initialize()方法,提醒在使用对象之前必须调用这个方法,使得类的设计者可以确保每个对象都可以被初始化. 构造函数是没有返回类型的函数,用于 ...
- java学习笔记 java编程思想 第7章 复用类
文章目录 7.1 组合语法 练习1 7.2 继承语法 7.2.1 初始化基类 练习3 练习4 练习5 带参数的构造器 练习6 练习7 练习8 练习9 练习10 7.3 代理 练习11 7.4 结合使用 ...
- java学习笔记 java编程思想 第4章 控制执行流程
目录 4.1 true和false 4.2 if-else 4.3 迭代 4.3.1 do-while 4.3.2 for 练习1 练习2 练习3 练习4 练习5 4.3.3 逗号操作符 4.4 Fo ...
- java 学习笔记-网络编程(八)
网络编程 标签:学习各种网络协议的桥梁 什么是计算机网络 计算机网络的作用:资源共享和信息传递. 计算机网络的组成: a) 计算机硬件:计算机(大中小型服务器,台式机.笔记本等).外部设备(路由器.交 ...
最新文章
- 清华首批7门标杆课程,到底有多牛?
- 一键添加JAVA环境变量
- JDK在centos和Ubuntu 三种安装方式
- 成员变量 局部变量 类变量
- 3 Convex functions
- material 项目_Web开发必备的 10 个开源项目,不用自己亲自造轮子!
- MongoDB 数据库创建、删除、表(集合) 创建删除、数据的增、删、改、查
- 主进程中发生javascript错误_你知道 JavaScript 中的错误对象有哪些类型吗?
- 240多个jquery插件
- 有关linux下find和xargs的使用
- jedis 用连接池时超时返回值类型错误
- javaweb实训第一天上午——HTML和CSS
- SpringBoot23 分模块开发
- JavaScript高级教程——(19)构造函数、原型、原型链、继承
- MTK平台Camera图片的Exif信息
- 图片干扰背景处理,简单易懂
- 风暴英雄 服务器在哪个文件夹,《风暴英雄》国服官方答疑 玩家最关心的问题都在这里...
- 游戏本自动掉帧_实用 | 大夏天,如何解决卡顿掉帧?
- 链家网深圳租房信息分析报告
- 微信会员系统怎么做?如何建立全方位会员营销体系?