Java编程思想(第四版)——初始化与清理(1)

初始化与清理的深入学习与理解(1)

Posted by yuchen on April 12, 2016
ch5-1.html

Java编程思想(第四版)学习笔记


第五章 初始化与清理(1)

1.用构造器确保初始化

在Java中,“初始化”和“创建”捆绑在一起,两者不能分离。

2.构造器没有返回值,这与返回值为空(void)明显不同。
3.方法重载:方法名相同而形式参数不同。

注意:
1. 甚至形参顺序的不同也足以区分两个方法,如下示例:

public class OverloadingOrder {
  static void f(String s, int i) {
    print("String: " + s + ", int: " + i);
  }
  static void f(int i, String s) {
    print("int: " + i + ", String: " + s);
  }
  public static void main(String[] args) {
    f("String first", 11);
    f(99, "Int first");
  }
} /* Output:
String: String first, int: 11
int: 99, String: Int first
*///:~

一般不要这样做,因为这会使代码难以维护。
2. 根据方法返回值来区分重载方法是行不通的,因为当调用方法但是不关心返回值时,编译器无法知晓该调用哪一种形式,如程序中仅仅写出如下语句:

void f(){}
int f(){return 1;}
public static void main(String[] args){
    f();
}

f();不需要利用返回值,仅仅是调用该函数,得到其他效果。

4.关于方法重载的传参
  • 整型常量被当做int值处理,即选择接受int型参数的方法。
  • 如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型就会被提升。
  • char型略有不同,如果无法找到恰好接受char型参数的方法,就会把char直接提升至int型。
  • 方法接受较小的基本类型作为参数。如果传入的实际参数较大,就得通过强制类型转换来执行窄化转换,否则,编译器会报错。
5.默认构造器
  • 如果你写的类中没有构造器,则编译器会自动帮你创建一个默认构造器。
  • 但是,如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认构造器。
6.this关键字

可以在一条语句中定义类对象:

Banana a = new Banana(),
       b = new Banana();

this 关键字只能在方法内部使用,表示对”调用方法的那个对象”的引用。注意,如果在方法内部调用同一个类的的另一个方法,就不必使用this,直接调用即可。人们期望只在必要处使用this。

7.在构造器中调用构造器

通常写this表示对当前对象的引用。但是,在构造器中,如果为this添加参数列表,便产生对符合此参数列表的某个构造器的明确调用。
注意:

  • 尽管可以用this调用一个构造器,但却不能调用两个。
  • 必须将构造器置于最起始处,否则编译器会报错。
  • 构造器参数的名称和数据成员的名称相同时,使用this.数据成员名称解决冲突。
  • 除构造器以外,编译器禁止在其他任何方法中通过this调用构造器。
8.static含义

static方法就是没有this的方法,在static方法的内部不能调用非静态方法(不是完全不可能:若你传递一个对象的引用到静态方法中(静态方法可以创建其自身的对象),然后通过这个引用就可以调用非静态方法和访问非静态成员了。但是通常要达到这样的效果,只需要写一个非静态的方法即可。),反过来倒是可以。static方法可以在没有创建任何对象的前提下,仅仅通过类本身来调用,与Smalltalk语言的”类方法”相对应。

9.清理:终结处理–finalize()
  • finalize()不是C++中的析构函数,因为无论对象如何创建(即使是对象中含有其他对象的这种情况),垃圾回收器都会负责释放对象占据的所有内存。之所以要有finalize(),是由于在分配内存时可能采用了类似C语言的做法,而非Java中的通常做法。这种情况逐一发生在使用”本地方法”的情况下,本地方法是一种在Java中调用非Java代码的方式。本地方法目前只支持C和C++,但是它们可以调用其他语言写的代码,所以实际上可以调用任何代码。在非Java代码中,也许会调用C的malloc()函数系列来分配存储空间,所以需要在finalize()中用本地方法调用free()。
  • finalize()的工作原理:一旦垃圾回收器准备好释放对象占用的存储空间,将首先调用其finalize()方法,并且在下一次垃圾回收动作发生时,才会真正回收对象占用的内存。

    原文:When the garbage collector is ready to release the storage used for your object, it will first call finalize( ), and only on the next garbage-collection pass will it reclaim the object’s memory.

  • Java里的对象并非总是被垃圾回收,记住三点:
    • 1.对象可能不被垃圾回收。
    • 2.垃圾回收并不等于析构。
    • 3.垃圾回收只与内存有关。
    • 注意
      a.只要程序没有濒临存储空间用完的那一刻,对象占用的空间就总也得不到释放。如果程序执行结束,并且垃圾回收器一直没有释放你创建的任何对象的存储空间,则随着程序的退出,那些资源也会全部交还给操作系统(针对1、2的说明)。
      b.使用垃圾回收器的唯一原因就是为了回收程序不再使用的内存。所以对于与垃圾回收有关的任何行为来说(尤其是finalize()方法),它们也必须同内存及其回收有关。
  • 最后,终结函数无法预料,常常是危险的,总之是多余的。
10.清理:垃圾回收
  • Java不允许创建局部对象,必须使用new创建对象。在Java中,没有用于释放对象的delete,因为垃圾回收器会帮助你释放存储空间。然而,垃圾回收器的存在并不能完全代替析构函数(当然,绝对不能直接调用finalize(),所以,这也不是一种解决方案)。如果希望进行除释放存储空间之外的清理工作,还是得明确调用某个恰当的Java方法。这就等同于使用析构函数了,只是没有它方便。通常,并不能指望finalize(),必须创建其他的”清理”方法,并明确的调用它们。
  • 记住,无论是”垃圾回收”还是”终结”,都不保证一定会发生。如果JVM并未面临内存耗尽的情形,它是不会浪费时间去执行垃圾回收以恢复内存的。
  • 只要对象存在没有被适当清理的部分,程序就存在很隐晦的缺陷,finalize()可以用来最终发现这种情况——尽管它并不总是被调用。
  • finalize()使用案例:
//: initialization/TerminationCondition.java
package io.github.wzzju; /* Added by Eclipse.py */
// Using finalize() to detect an object that
// hasn't been properly cleaned up.

class Book {
  boolean checkedOut = false;
  Book(boolean checkOut) {
    checkedOut = checkOut;
  }
  void checkIn() {
    checkedOut = false;
  }
  void checkOut() {
        checkedOut = true;
      }
  protected void finalize() {
    try {
        super.finalize();
    } catch (Throwable e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    if(checkedOut)
      System.out.println("Error: checked out");
    // Normally, you'll also do this:
    // super.finalize(); // Call the base-class version
  }
}

public class TerminationCondition {
  public static void main(String[] args) {
    Book novel = new Book(true);
    // Proper cleanup:
    novel.checkIn();
    novel.checkOut();
    // Drop the reference, forget to clean up:
    new Book(true);
    // Force garbage collection & finalization:
    System.gc();
  }
} /* Output:
Error: checked out
*///:~

注意:
1.System.gc()用于强制进行垃圾回收和终结动作(finalize())。
2.总应假设基类版的finalize()也要做某些重要的事,因此与使用super来调用它。
3.即使novel的checkedOut字段值也为true,但是却未输出,finalize()此时未调用(why?是因为还有引用指向对象实体,其为活的实体,故而不回收?)。

11.垃圾回收器如何工作?
  • Java中所有对象(基本类型除外)都在堆上分配。
  • 垃圾回收器对于提高对象的创建速度有明显的效果。这意味着,Java从堆中分配空间的速度,可以和其他语言从栈上分配空间的速度相媲美。

    It means that allocating storage for heap objects in Java can be nearly as fast as creating storage on the stack in other languages.

  • 垃圾回收器的工作原理:

    For example, you can think of the C++ heap as a yard where each object stakes out its own piece of turf. This real estate can become abandoned sometime later and must be reused. In some JVMs, the Java heap is quite different; it’s more like a conveyor belt that moves forward every time you allocate a new object. This means that object storage allocation is remarkably rapid. The “heap pointer” is simply moved forward into virgin territory, so it’s effectively the same as C++’s stack allocation. (Of course, there’s a little extra overhead for bookkeeping, but it’s nothing like searching for storage.) You might observe that the heap isn’t in fact a conveyor belt, and if you treat it that way, you’ll start paging memory—moving it on and off disk, so that you can appear to have more memory than you actually do. Paging significantly impacts performance. Eventually, after you create enough objects, you’ll run out of memory. The trick is that the garbage collector steps in, and while it collects the garbage it compacts all the objects in the heap so that you’ve effectively moved the “heap pointer” closer to the beginning of the conveyor belt and farther away from a page fault. The garbage collector rearranges things and makes it possible for the high-speed, infinite-free-heap model to be used while allocating storage. To understand garbage collection in Java, it’s helpful learn how garbage-collection schemes work in other systems. A simple but slow garbage-collection technique is called reference counting. This means that each object contains a reference counter, and every time a reference is attached to that object, the reference count is increased. Every time a reference goes out of scope or is set to null, the reference count is decreased. Thus, managing reference counts is a small but constant overhead that happens throughout the lifetime of your program. The garbage collector moves through the entire list of objects, and when it finds one with a reference count of zero it releases that storage (however, reference counting schemes often release an object as soon as the count goes to zero). The one drawback is that if objects circularly refer to each other they can have nonzero reference counts while still being garbage. Locating such self-referential groups requires significant extra work for the garbage collector. Reference counting is commonly used to explain one kind of garbage collection, but it doesn’t seem to be used in any JVM implementations. In faster schemes, garbage collection is not based on reference counting. Instead, it is based on the idea that any non-dead object must ultimately be traceable back to a reference that lives either on the stack or in static storage. The chain might go through several layers of objects. Thus, if you start in the stack and in the static storage area and walk through all the references, you’ll find all the live objects. For each reference that you find, you must trace into the object that it points to and then follow all the references in that object, tracing into the objects they point to, etc., until you’ve moved through the entire Web that originated with the reference on the stack or in static storage. Each object that you move through must still be alive. Note that there is no problem with detached self-referential groups—these are simply not found, and are therefore automatically garbage. In the approach described here, the JVM uses an adaptive garbage-collection scheme, and what it does with the live objects that it locates depends on the variant currently being used. One of these variants is stop-and-copy. This means that—for reasons that will become apparent—the program is first stopped (this is not a background collection scheme). Then, each live object is copied from one heap to another, leaving behind all the garbage. In addition, as the objects are copied into the new heap, they are packed end-to-end, thus compacting the new heap (and allowing new storage to simply be reeled off the end as previously described). Of course, when an object is moved from one place to another, all references that point at the object must be changed. The reference that goes from the heap or the static storage area to the object can be changed right away, but there can be other references pointing to this object that will be encountered later during the “walk.” These are fixed up as they are found (you could imagine a table that maps old addresses to new ones). There are two issues that make these so-called “copy collectors” inefficient. The first is the idea that you have two heaps and you slosh all the memory back and forth between these two separate heaps, maintaining twice as much memory as you actually need. Some JVMs deal with this by allocating the heap in chunks as needed and simply copying from one chunk to another. The second issue is the copying process itself. Once your program becomes stable, it might be generating little or no garbage. Despite that, a copy collector will still copy all the memory from one place to another, which is wasteful. To prevent this, some JVMs detect that no new garbage is being generated and switch to a different scheme (this is the “adaptive” part). This other scheme is called mark-and-sweep, and it’s what earlier versions of Sun’s JVM used all the time. For general use, mark-and-sweep is fairly slow, but when you know you’re generating little or no garbage, it’s fast. Mark-and-sweep follows the same logic of starting from the stack and static storage, and tracing through all the references to find live objects. However, each time it finds a live object, that object is marked by setting a flag in it, but the object isn’t collected yet. Only when the marking process is finished does the sweep occur. During the sweep, the dead objects are released. However, no copying happens, so if the collector chooses to compact a fragmented heap, it does so by shuffling objects around. “Stop-and-copy” refers to the idea that this type of garbage collection is not done in the background; instead, the program is stopped while the garbage collection occurs. In the Sun literature you’ll find many references to garbage collection as a low-priority background process, but it turns out that the garbage collection was not implemented that way in earlier versions of the Sun JVM. Instead, the Sun garbage collector stopped the program when memory got low. Mark-and-sweep also requires that the program be stopped. As previously mentioned, in the JVM described here memory is allocated in big blocks. If you allocate a large object, it gets its own block. Strict stop-and-copy requires copying every live object from the source heap to a new heap before you can free the old one, which translates to lots of memory. With blocks, the garbage collection can typically copy objects to dead blocks as it collects. Each block has a generation count to keep track of whether it’s alive. In the normal case, only the blocks created since the last garbage collection are compacted; all other blocks get their generation count bumped if they have been referenced from somewhere. This handles the normal case of lots of short-lived temporary objects. Periodically, a full sweep is made—large objects are still not copied (they just get their generation count bumped), and blocks containing small objects are copied and compacted. The JVM monitors the efficiency of garbage collection and if it becomes a waste of time because all objects are long-lived, then it switches to mark-andsweep. Similarly, the JVM keeps track of how successful mark-and-sweep is, and if the heap starts to become fragmented, it switches back to stop-and-copy. This is where the “adaptive” part comes in, so you end up with a mouthful: “Adaptive generational stop-and-copy mark-andsweep.” There are a number of additional speedups possible in a JVM. An especially important one involves the operation of the loader and what is called a just-in-time (JIT) compiler. A JIT compiler partially or fully converts a program into native machine code so that it doesn’t need to be interpreted by the JVM and thus runs much faster. When a class must be loaded (typically, the first time you want to create an object of that class), the .class file is located, and the bytecodes for that class are brought into memory. At this point, one approach is to simply JIT compile all the code, but this has two drawbacks: It takes a little more time, which, compounded throughout the life of the program, can add up; and it increases the size of the executable (bytecodes are significantly more compact than expanded JIT code), and this might cause paging, which definitely slows down a program. An alternative approach is lazy evaluation, which means that the code is not JIT compiled until necessary. Thus, code that never gets executed might never be JIT compiled. The Java HotSpot technologies in recent JDKs take a similar approach by increasingly optimizing a piece of code each time it is executed, so the more the code is executed, the faster it gets.

  • 问题1.对象在堆中分配,对象的引用在哪?(stack or static storage area?)

  • 问题2.对象中的引用在哪?(heap?)

本文总阅读量