目录

  • 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中对象不一定总是被垃圾回收。也就是说:

  1. 对象可能不被垃圾回收。
  2. 垃圾回收并不等于“析构”。

5.5.1 finalize()的用途何在

  1. 垃圾回收只与内存相关。

使用垃圾回收器的原因是为了回收程序不再使用的内存。垃圾回收器负责释放对象占据的所有内存。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章 初始化与清理相关推荐

  1. java编程思想初始化引用,JAVA编程思想--第5章 初始化与清理

    随着计算机革命的发展,"不安全"的编程方式已逐渐成为编程代价高昂的主因之一. 初始化和清理(cleanup)是涉及安全的两个问题.初始化时,忘记初始化时许多错误的来源,还有就是不知 ...

  2. java学习笔记 java编程思想 第6章 访问权限控制

    文章目录 6.1 包:库单元(the library unit) 6.1.1 代码组织 6.1.2 创建独一无二的包名 练习1 练习2 6.1.3 定制工具类 6.1.4 用import改变行为 练习 ...

  3. Java学习笔记-网络编程

    Java提供了网络编程,并且在实际中有着大量运用 网络编程 网络编程概述 网络模型 OSI参考模型 TCP/IP参考模型 网络通讯要素 IP地址 端口号 传输协议 网络参考模型 网络通讯要素 IP地址 ...

  4. java学习笔记—java的学习路线

    Java体系涉及到三个方面:J2SE,J2EE,J2ME(KJAVA). J2SE,Java 2 Platform Standard Edition,我们经常说到的JDK,就主要指的这个,它是三者的基 ...

  5. 狂神说Java学习笔记 Java基础

    目录 机器语言 第二代语言(汇编语言) 第三代语言 高级语言 Java特性和优势 JDK(Java Development Kit) JRE(Java Runtime Enviroment) JVM( ...

  6. Java学习笔记--Java中必记常见异常

    JAVA常见异常 Java.io.NullPointerException null 空的,不存在的 NullPointer 空指针 空指针异常,该异常出现在我们操作某个对象的属性或方法时,如果该对象 ...

  7. 一起读Java编程思想(2)---构造器的初始化与清理

    初始化与清理 用构造器确保初始化 每个类都要定义一个initialize()方法,提醒在使用对象之前必须调用这个方法,使得类的设计者可以确保每个对象都可以被初始化. 构造函数是没有返回类型的函数,用于 ...

  8. java学习笔记 java编程思想 第7章 复用类

    文章目录 7.1 组合语法 练习1 7.2 继承语法 7.2.1 初始化基类 练习3 练习4 练习5 带参数的构造器 练习6 练习7 练习8 练习9 练习10 7.3 代理 练习11 7.4 结合使用 ...

  9. 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 ...

  10. java 学习笔记-网络编程(八)

    网络编程 标签:学习各种网络协议的桥梁 什么是计算机网络 计算机网络的作用:资源共享和信息传递. 计算机网络的组成: a) 计算机硬件:计算机(大中小型服务器,台式机.笔记本等).外部设备(路由器.交 ...

最新文章

  1. 清华首批7门标杆课程,到底有多牛?
  2. 一键添加JAVA环境变量
  3. JDK在centos和Ubuntu 三种安装方式
  4. 成员变量 局部变量 类变量
  5. 3 Convex functions
  6. material 项目_Web开发必备的 10 个开源项目,不用自己亲自造轮子!
  7. MongoDB 数据库创建、删除、表(集合) 创建删除、数据的增、删、改、查
  8. 主进程中发生javascript错误_你知道 JavaScript 中的错误对象有哪些类型吗?
  9. 240多个jquery插件
  10. 有关linux下find和xargs的使用
  11. jedis 用连接池时超时返回值类型错误
  12. javaweb实训第一天上午——HTML和CSS
  13. SpringBoot23 分模块开发
  14. JavaScript高级教程——(19)构造函数、原型、原型链、继承
  15. MTK平台Camera图片的Exif信息
  16. 图片干扰背景处理,简单易懂
  17. 风暴英雄 服务器在哪个文件夹,《风暴英雄》国服官方答疑 玩家最关心的问题都在这里...
  18. 游戏本自动掉帧_实用 | 大夏天,如何解决卡顿掉帧?
  19. 链家网深圳租房信息分析报告
  20. 微信会员系统怎么做?如何建立全方位会员营销体系?

热门文章

  1. 未来几年,自动化发展趋势展望
  2. JSP七动作---<jsp:setProperty>
  3. 【Uplift】评估方法篇
  4. 爱普生L4168喷墨打印机打印断线或堵头故障处理
  5. 探究施乐打印机新功能
  6. web开发字体图标制作
  7. 微博视频号搬砖项目,单号月入1000+!
  8. 想问问,数模小白三个月准备数模国赛,现实吗?
  9. win7系统备份还原软件_比ghost快200%!备份还原系统真正首选的神器
  10. Java学习指南(15) 链表