java学习笔记 java编程思想 第5章 初始化与清理


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

  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>方法去检测对象是否真确被清理。
     */
    @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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值