目录
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.class
Last modified 2021-4-3; size 430 bytes
MD5 checksum 53fdb1829eb6d6282d5a81860cfe9843
Compiled from "Exec07.java"
public class com.qww.exec07.Exec07
minor version: 0
major version: 52
flags: 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: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/qww/exec07/Exec07;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: new #2 // class com/qww/exec07/A
3: dup
4: invokespecial #3 // Method com/qww/exec07/A."<init>":()V
7: pop
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 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>方法去检测对象是否真确被清理。
*/
@Override
protected 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"};
@Override
protected 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.");
}
@Override
protected 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数组的长度为2
printArr(1, "asdf", "jkl;");
// 也可以不传入可变长参数,args数组的长度为0
printArr(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 list
public static void f(String... args, int a) {
}
*/
}
// 运行结果
a=1
asdf jkl;
length: 2
a=111
length: 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