Initialization and cleaning in JDK8

Article catalog

Initialization and cleanup

"Unsafe" programming is one of the main reasons for the high cost of programming. There are two security issues: initialization and cleanup. Many bug s in C are caused by programmers forgetting to initialize. In particular, many class library users don't know how to initialize class library components, or even they have to initialize them. Cleaning is another special problem, because you don't care about an element when you use it, so it's easy to forget to clean it. In this way, the resources used by the elements will not be recycled until the program consumes all the resources (especially memory).

C + + introduces the concept of constructor, which is a special method. Every time an object is created, the method will be called automatically. Java uses the concept of a constructor, and also uses a garbage collector (GC) to automatically reclaim resources occupied by objects that are no longer being used. This chapter discusses initialization and cleanup issues, as well as support for them in Java.

Using constructor to ensure initialization

You may want to create an initialize() method for each class, which implies that you need to call the class before using it. Unfortunately, users have to remember to call it. In Java, the designer of a class guarantees the initialization of each object through a constructor. If a class has a constructor, Java will automatically call the constructor method of the object before the user uses the object (that is, the object has just been created), so as to ensure initialization. The next challenge is to name the constructor method. There are two problems: the first is that any naming may conflict with the naming of other existing elements in the class; the second is that the compiler must always know the constructor method name to call it. The C + + solution seems to be the simplest and most logical, so the same approach is used in Java: the constructor name is the same as the class name. It makes sense to call constructor methods automatically during initialization.

The following example is a class that contains a constructor:

// housekeeping/SimpleConstructor.java
// Demonstration of a simple constructor

class Rock {
    Rock() { // This is a constructor
        System.out.print("Rock ");
    }
}

public class SimpleConstructor {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Rock();
        }
    }
}

Output:

Rock Rock Rock Rock Rock Rock Rock Rock Rock Rock 

Now, when you create an object: new Rock(), memory is allocated and the constructor is called. The constructor ensures that the object is properly initialized before you use it.

It should be noted that the constructor method name is the same as the class name and does not need to conform to the programming style of lowercase. In C + +, nonparametric constructors are called default constructors, a term that was used for many years before Java. However, for some reasons, Java designers decided to use the name parameterless constructor, which I (the author) thought was clumsy and unnecessary, so I intend to continue to use the default constructor. Java 8 introduces the method of modifying the default keyword, so anyway, I'll use the name of parameterless constructor.

Like other methods, constructor methods can pass in parameters to define how to create an object. The previous example is slightly modified so that the constructor receives a parameter:

// housekeeping/SimpleConstructor2.java
// Constructors can have arguments

class Rock2 {
    Rock2(int i) {
        System.out.print("Rock " + i + " ");
    }
}

public class SimpleConstructor2 {
    public static void main(String[] args) {
        for (int i = 0; i < 8; i++) {
            new Rock2(i);
        }
    }
}

Output:

Rock 0 Rock 1 Rock 2 Rock 3 Rock 4 Rock 5 Rock 6 Rock 7

If the Tree class has a constructor that only accepts one parameter to represent the height of the Tree, you can create a Tree as follows:

Tree t = new Tree(12); // 12 foot tree

If Tree(int) is the only constructor, the compiler does not allow you to create Tree type objects in any other way.

Constructors eliminate an important problem and make code easier to read. For example, in the code block above, you don't see an explicit call to the initialize() method, which conceptually should be separated from the creation of the object. In Java, the creation and initialization of objects are unified concepts, and they are inseparable.

The constructor does not return a value, it is a special method. But it is different from the common method with void return type. The common method can return null value, and you can choose to let it return other types. While the constructor has no return value, but there is no choice for you (although the new expression returns the reference of the newly created object, the constructor itself does not return any value). If it has a return value, and you can choose what you want it to return, then the compiler has to know what to do with that return value next (it has no receiver).

Method overload

Naming is an important feature of any programming language. When you create an object, you name the memory space allocated to it. Method is the name of the behavior. You refer to all objects, properties, and methods by name. A well named system is easy to understand and modify. It's like writing prose - to communicate with readers.

Mapping nuances of human languages to programming languages creates a problem. Usually, the same words can express many different meanings - they are "overloaded". This is especially useful when the difference in meaning is small. You'll say "wash the shirt," "wash the car," and "wash the dog.". It would be foolish to say that: "wash the shirt by washing the shirt", "wash the car by washing the car" and "wash the dog by washing the dog", because the audience doesn't need to distinguish the action at all. Most human languages are "redundant," so you can understand the meaning even if you miss a few words. You don't need to use different words for each concept - you can infer meaning from the context.

Most programming languages, especially C, require a unique identifier for each method, often called a function in these languages. So, you can't have a print() function that prints both integer and floating-point types -- each function name has to be different.

In Java (C + +), there is also a factor that forces the use of method overloading: constructors. Because the constructor method name must be the same as the class name, there will only be one constructor name in a class. So how do you create an object in different ways? For example, if you want to create a class, there are two ways to initialize it: one is to standardize it, the other is to read information from a file. You need two constructors: a nonparametric constructor and a constructor with a String type parameter that is passed in the filename. Both constructors have the same name - the same as the class name. Therefore, method overloading is necessary, which allows methods to have the same method name but receive different parameters. Although method overloading is important for constructors, it is also convenient to overload any method.

The following example shows how to overload constructors and methods:

// housekeeping/Overloading.java
// Both constructor and ordinary method overloading

class Tree {
    int height;
    Tree() {
        System.out.println("Planting a seedling");
        height = 0;
    }
    Tree(int initialHeight) {
        height = initialHeight;
        System.out.println("Creating new Tree that is " + height + " feet tall");
    }
    void info() {
        System.out.println("Tree is " + height + " feet tall");
    }
    void info(String s) {
        System.out.println(s + ": Tree is " + height + " feet tall");
    }
}
public class Overloading {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Tree t = new Tree(i);
            t.info();
            t.info("overloaded method");
        }
        new Tree(); 
    }
}

Output:

Creating new Tree that is 0 feet tall
Tree is 0 feet tall
overloaded method: Tree is 0 feet tall
Creating new Tree that is 1 feet tall
Tree is 1 feet tall
overloaded method: Tree is 1 feet tall
Creating new Tree that is 2 feet tall
Tree is 2 feet tall
overloaded method: Tree is 2 feet tall
Creating new Tree that is 3 feet tall
Tree is 3 feet tall
overloaded method: Tree is 3 feet tall
Creating new Tree that is 4 feet tall
Tree is 4 feet tall
overloaded method: Tree is 4 feet tall
Planting a seedling

A Tree object can be a sapling, created with a non parametric constructor, or a Tree that has grown up in a greenhouse, and has a certain height. In this case, it needs to be created with a parametric constructor.

You may want to call the info() method in several ways. For example, if you want to print additional messages, you can use the info(String) method. If you have nothing to say, you can use the info() method. It seems strange to define exactly the same concept with two names, but with method overloading, you can define a concept with one name.

Distinguish overload method

If two methods are named the same, how does Java know which one you are calling? There is a simple rule: each overloaded method must have a unique parameter list. If you think about it a little bit, it will be clear that there is no other way to distinguish two methods of the same name except through different parameter lists. You can even differentiate between different methods based on the order of the parameters in the parameter list, although this makes the code difficult to maintain. For example:

// housekeeping/OverloadingOrder.java
// Overloading based on the order of the arguments

public class OverloadingOrder {
    static void f(String s, int i) {
        System.out.println("String: " + s + ", int: " + i);
    }

    static void f(int i, String s) {
        System.out.println("int: " + i + ", String: " + s);
    }

    public static void main(String[] args) {
        f("String first", 1);
        f(99, "Int first");
    }
}

Output:

String: String first, int: 1
int: 99, String: Int first

Two f() methods have the same parameters, but the order of the parameters is different. According to this, they can be distinguished.

Overloading and basic types

Basic types can automatically transition from smaller to larger types. When this is combined with overloading, it's a bit confusing. Here's an example:

// housekeeping/PrimitiveOverloading.java
// Promotion of primitives and overloading

public class PrimitiveOverloading {
    void f1(char x) {
        System.out.print("f1(char)");
    }
    void f1(byte x) {
        System.out.print("f1(byte)");
    }
    void f1(short x) {
        System.out.print("f1(short)");
    }
    void f1(int x) {
        System.out.print("f1(int)");
    }
    void f1(long x) {
        System.out.print("f1(long)");
    }
    void f1(float x) {
        System.out.print("f1(float)");
    }
    void f1(double x) {
        System.out.print("f1(double)");
    }
    void f2(byte x) {
        System.out.print("f2(byte)");
    }
    void f2(short x) {
        System.out.print("f2(short)");
    }
    void f2(int x) {
        System.out.print("f2(int)");
    }
    void f2(long x) {
        System.out.print("f2(long)");
    }
    void f2(float x) {
        System.out.print("f2(float)");
    }
    void f2(double x) {
        System.out.print("f2(double)");
    }
    void f3(short x) {
        System.out.print("f3(short)");
    }
    void f3(int x) {
        System.out.print("f3(int)");
    }
    void f3(long x) {
        System.out.print("f3(long)");
    }
    void f3(float x) {
        System.out.print("f3(float)");
    }
    void f3(double x) {
        System.out.print("f3(double)");
    }
    void f4(int x) {
        System.out.print("f4(int)");
    }
    void f4(long x) {
        System.out.print("f4(long)");
    }
    void f4(float x) {
        System.out.print("f4(float)");
    }
    void f4(double x) {
        System.out.print("f4(double)");
    }
    void f5(long x) {
        System.out.print("f5(long)");
    }
    void f5(float x) {
        System.out.print("f5(float)");
    }
    void f5(double x) {
        System.out.print("f5(double)");
    }
    void f6(float x) {
        System.out.print("f6(float)");
    }
    void f6(double x) {
        System.out.print("f6(double)");
    }
    void f7(double x) {
        System.out.print("f7(double)");
    }
    void testConstVal() {
        System.out.print("5: ");
        f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5);
        System.out.println();
    }
    void testChar() {
        char x = 'x';
        System.out.print("char: ");
        f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
        System.out.println();
    }
    void testByte() {
        byte x = 0;
        System.out.print("byte: ");
        f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
        System.out.println();
    }
    void testShort() {
        short x = 0;
        System.out.print("short: ");
        f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
        System.out.println();
    }
    void testInt() {
        int x = 0;
        System.out.print("int: ");
        f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
        System.out.println();
    }
    void testLong() {
        long x = 0;
        System.out.print("long: ");
        f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
        System.out.println();
    }
    void testFloat() {
        float x = 0;
        System.out.print("float: ");
        f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
        System.out.println();
    }
    void testDouble() {
        double x = 0;
        System.out.print("double: ");
        f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
        System.out.println();
    }

    public static void main(String[] args) {
        PrimitiveOverloading p = new PrimitiveOverloading();
        p.testConstVal();
        p.testChar();
        p.testByte();
        p.testShort();
        p.testInt();
        p.testLong();
        p.testFloat();
        p.testDouble();
    }
}

Output:

5: f1(int)f2(int)f3(int)f4(int)f5(long)f6(float)f7(double)
char: f1(char)f2(int)f3(int)f4(int)f5(long)f6(float)f7(double)
byte: f1(byte)f2(byte)f3(short)f4(int)f5(long)f6(float)f7(double)
short: f1(short)f2(short)f3(short)f4(int)f5(long)f6(float)f7(double)
int: f1(int)f2(int)f3(int)f4(int)f5(long)f6(float)f7(double)
long: f1(long)f2(long)f3(long)f4(long)f5(long)f6(float)f7(double)
float: f1(float)f2(float)f3(float)f4(float)f5(float)f6(float)f7(double)
double: f1(double)f2(double)f3(double)f4(double)f5(double)f6(double)f7(double)

If the type of parameter passed in is greater than the type of parameter expected to be received by the method, you must do the down conversion first. If you don't, the compiler will report an error.

Overload of return value

People often wonder, "why can't we distinguish methods by method names and return values, but only by method names and parameter lists?". For example, the following two methods have the same name and parameters, but they are easy to distinguish:

void f(){}
int f() {return 1;}

In some cases, the compiler can easily infer exactly which method to call from the context, such as int x = f().

However, you can call a method and ignore the return value. This is called a side effect of calling a function, because you don't care about the return value, just want to do something with the method. So if you call f() directly, the java compiler doesn't know which method you want to call, and the reader doesn't know. For this reason, you cannot distinguish overloaded methods based on the return value type. In order to support the new features, Java 8 improves the accuracy of guessing in some specific cases, but generally does not work.

Parameterless constructor

As mentioned earlier, a parameterless constructor is a constructor that does not accept parameters, which is used to create a "default object". If you create a class with no constructors in it, the compiler will automatically create a parameterless constructor for you. For example:

// housekeeping/DefaultConstructor.java
class Bird {}
public class DefaultConstructor {
    public static void main(String[] args) {
        Bird bird = new Bird(); // default
    }
}

The expression new Bird() creates a new object and calls a parameterless constructor, although there is no explicit definition of a parameterless constructor in the Bird class. Imagine how we could create an object without a constructor. However, once you explicitly define a constructor (with or without parameters), the compiler will not automatically create a parameterless constructor for you. As follows:

// housekeeping/NoSynthesis.java
class Bird2 {
    Bird2(int i) {}
    Bird2(double d) {}
}
public class NoSynthesis {
    public static void main(String[] args) {
        //- Bird2 b = new Bird2(); // No default
        Bird2 b2 = new Bird2(1);
        Bird2 b3 = new Bird2(1.0);
    }
}

If you call new Bird2(), the compiler prompts that no matching constructor can be found. When there is no constructor in the class, the compiler will say, "you must need a constructor, so let me create one for you.". But if there is a constructor in the class, the compiler will say, "you have written the constructor, so you must know what you are doing. If you don't create the default constructor, you don't need it.".

this keyword

For two objects of the same type a and b, you may be wondering how to call the peel() method of these two objects:

// housekeeping/BananaPeel.java

class Banana {
    void peel(int i) {
        /*...*/
    }
}
public class BananaPeel {
    public static void main(String[] args) {
        Banana a = new Banana(), b = new Banana();
        a.peel(1);
        b.peel(2);
    }
}

If there is only one method, peel(), how do you know whether it is the peel() method of object a or the peel() method of object b? The compiler does some basic work, so you can write code like this. The first parameter in the peel() method implicitly passes in a

quote. Therefore, the method calls in the above example are as follows:

Banana.peel(a, 1)
Banana.peel(b, 1)

This is implemented internally. You can't write code like this directly. The compiler won't accept it, but it can explain what happened. Let's say that now inside the method, you want to get a reference to the current object. However, object references are passed to the compiler in secret -- not in the parameter list. Conveniently, there is a keyword: this. This keyword can only be used inside non static methods. When you call an object's method, this generates an object reference. You can treat this reference as you would any other reference. If you call other methods in a class's method, don't use this, just call it directly. This is automatically applied to other methods. So you can do something like this:

// housekeeping/Apricot.java

public class Apricot {
    void pick() {
        /* ... */
    }

    void pit() {
        pick();
        /* ... */
    }
}

In the pit() method, you can use the this.pick(), but not necessary. The compiler does this for you automatically. This keyword is only used in special cases where the current object reference must be explicitly used. For example, to return a reference to the current object in a return statement.

// housekeeping/Leaf.java
// Simple use of the "this" keyword

public class Leaf {

    int i = 0;

    Leaf increment() {
        i++;
        return this;
    }

    void print() {
        System.out.println("i = " + i);
    }

    public static void main(String[] args) {
        Leaf x = new Leaf();
        x.increment().increment().increment().print();
    }
}

Output:

i = 3

Because increment() returns a reference to the current object through this keyword, you can easily perform multiple operations on the same object.

this keyword is also useful when passing the current object to other methods:

// housekeeping/PassingThis.java

class Person {
    public void eat(Apple apple) {
        Apple peeled = apple.getPeeled();
        System.out.println("Yummy");
    }
}

public class Peeler {
    static Apple peel(Apple apple) {
        // ... remove peel
        return apple; // Peeled
    }
}

public class Apple {
    Apple getPeeled() {
        return Peeler.peel(this);
    }
}

public class PassingThis {
    public static void main(String[] args) {
        new Person().eat(new Apple());
    }
}

Output:

Yummy

Apple has to call an external tool method for some reason (for example, the method in a tool class appears repeatedly in multiple classes, you don't want the code to repeat) Peeler.peel() do something. You must use this to pass yourself to an external method.

Invoke constructors in the constructor

When you write multiple constructors in a class, sometimes you want to call another constructor in one constructor to avoid code duplication. You implement such a call through this keyword.

Usually when you say this, it means "this object" or "current object", which itself generates a reference to the current object. In a constructor, when you give this a list of parameters, it means something else. It explicitly calls the constructor of the matching parameter list in the most direct way:

// housekeeping/Flower.java
// Calling constructors with "this"

public class Flower {
    int petalCount = 0;
    String s = "initial value";

    Flower(int petals) {
        petalCount = petals;
        System.out.println("Constructor w/ int arg only, petalCount = " + petalCount);
    }

    Flower(String ss) {
        System.out.println("Constructor w/ string arg only, s = " + ss);
        s = ss;
    }

    Flower(String s, int petals) {
        this(petals);
        //- this(s); // Can't call two!
        this.s = s; // Another use of "this"
        System.out.println("String & int args");
    }

    Flower() {
        this("hi", 47);
        System.out.println("no-arg constructor");
    }

    void printPetalCount() {
        //- this(11); // Not inside constructor!
        System.out.println("petalCount = " + petalCount + " s = " + s);
    }

    public static void main(String[] args) {
        Flower x = new Flower();
        x.printPetalCount();
    }
}

Output:

Constructor w/ int arg only, petalCount = 47
String & int args
no-arg constructor
petalCount = 47 s = hi

As can be seen from the constructor Flower(String s, int petals), the constructor can only be called once through this. In addition, the constructor must be called first, or the compiler will report an error. This example also shows another use of this. The variable name s in the parameter list is the same as the member variable name s, which can cause confusion. You can avoid repetition by indicating that you are referring to the member variable s through this.s. You will often see this usage in Java code, and it will appear many times in this book. In the printPetalCount() method, the compiler does not allow you to call a constructor in a method other than a constructor.

Meaning of static

Remember the content of this keyword, you will have a deeper understanding of the static decorated method: this will not exist in the static method. You can't call non static methods in static methods (otherwise you can). Static methods are created for classes and do not require any objects. In fact, this is the main purpose of static methods. Static methods look like global methods, but global methods are not allowed in Java. Static methods in one class can be accessed by other static methods and static properties. Some people think that static methods are not object-oriented because they do have the semantics of global methods. Use the static method, because there is no this, so you do not send a message to an object. Indeed, if you find a lot of static methods in your code, it's time to rethink your design. However, the concept of static is very practical, and it is often used. As for whether it is really "Object-Oriented", it is left to theorists to discuss.

Garbage collector

Programmers understand the importance of initialization, but often ignore the importance of cleanup. After all, who would clean up an int? But after using an object, it doesn't always matter whether it's safe. There is a garbage collector in Java to reclaim the memory occupied by useless objects. But now consider a special case: the object you create does not allocate memory through new, and the garbage collector only knows how to release the memory of the object created with new, so it does not know how to recycle the memory not allocated by new. To deal with this, Java allows you to define a method in your class called finalize().

Its working principle "assumes" that when the garbage collector is ready to reclaim the memory of an object, it will first call its finalize() method, and the memory occupied by the object will be really reclaimed when the next round of garbage collection occurs. So if you're going to use finalize (), you can do some important cleaning when you recycle. Finalize () is a potential programming trap, because some programmers (especially C + + programmers) will initially mistake it as a destructor in C + + (this function will be called when C + + destroys objects). So it is necessary to make a clear distinction: in C + +, objects are always destroyed (in a bug free program), while in Java, objects are not always garbage collected, or in other words:

  1. Object may not be garbage collected.
  2. Garbage collection is not equal to deconstruction.

This means that if you have to do something before you no longer need an object, you have to do it yourself. Java doesn't have a destructor or similar concept, so you have to create a common method to do this cleanup. For example, an object draws itself to the screen during creation. If it is not explicitly erased from the screen, it may never get cleaned up. If some erasure function is added to the finalize() method, when garbage collection occurs, the finalize() method is called (it is not guaranteed that it will happen), and the image will be erased. If "garbage collection" does not happen, the image will remain.

You may find that as long as the program is not running out of memory, the space occupied by objects will not be released. If the program ends and the garbage collector never frees up the memory of any objects you create, when the program exits, those resources will all be returned to the operating system. This strategy is appropriate because garbage collection itself has costs, and if you don't use it, you don't have to pay for that.

Purpose of finalize()

If you can't use finalize() as a general cleanup method, what's the use of this method?

This introduces the third point to remember:

  1. Garbage collection is only about memory.

In other words, the only reason to use garbage collection is to recycle the memory that the program no longer uses. So for any behavior related to garbage collection (especially the finalize() method), they must also be related to memory and its recycling.

But does this mean that the finalize () method should explicitly release other objects if they are included in the object? No, no matter how the object is created, the garbage collector is responsible for freeing all the memory occupied by the object. This limits the need for finalize() to a special case where storage space is allocated to objects in a way other than the way they are created. However, you may think that everything is an object in Java. How can this happen?

It seems that the finalize() method is available because it is possible to allocate memory in a way similar to C rather than Java. This situation mainly occurs in the case of using "local method", which is a form of calling non Java language code in Java language (for the discussion of local method, see Appendix B of electronic version 2 of this book). Local methods currently only support C and C + +, but they can call code written in other languages, so practically any code can be called. In non java code, the malloc() function family of C may be called to allocate storage space, and unless the free() function is called, the storage space will never be released, resulting in memory leakage. However, free () is a function in C and C + +, so you need to call it with a local method in the finalize() method.

After reading this, you may understand that you won't use the finalize() method too much. Yes, it's really not the right place to do ordinary cleaning. So, where is the general cleanup going?

You have to clean up

To clean up an object, the user must call the method that performs the cleanup action when the cleanup is needed. This sounds quite straightforward, but it slightly conflicts with the concept of "destructor" in C + +. In C + +, all objects are destroyed, or should be destroyed. If a local object is created in C + + (on the stack, not in Java), the destruction occurs at the end of the object scope bounded by the "right curly bracket". If the object is created with new (similar to Java), the corresponding destructor will be called when the programmer calls the delete operator of C + + (not in Java). If the programmer forgets to call delete, the destructor will never be called, which will cause memory leaks and other parts of the object will not be cleaned up. This kind of bug is hard to track, and it is also a major factor that makes C + + programmers turn to Java. Instead, in Java, there is no delete for freeing objects, because the garbage collector helps you free up storage space. It can even be said superficially that Java has no destructors because of garbage collection. However, as you learn more, you will understand that the existence of garbage collector does not completely replace the destructor (and you can never directly call finalize(), so this is not a solution either). If you want to clean up other than freeing up storage space, you need to explicitly call an appropriate Java method: This is equivalent to using destructors, but it is not convenient.

Remember, there's no guarantee that it will happen, whether it's "garbage collection" or "termination.". If the Java virtual machine (JVM) does not run out of memory, it may not waste time performing garbage collection to recover memory.

Termination conditions

In general, you can't expect finalize (), you have to create other "clean" methods and call them explicitly. So it seems that finalize () is only useful for some obscure memory cleaning that most programmers are hard to use. However, there is another interesting use of finalize(), which does not depend on every call to finalize(), which is the validation of object termination conditions.

When you are not interested in an object - that is, it will be cleaned up, the object should be in a state in which the memory it occupies can be safely released. For example, if an object represents an open file, the programmer should close the file before the object is garbage collected. As long as there are parts of the object that are not properly cleaned up, there are hidden bugs in the program. Finalize () can be used to eventually discover this, although it is not always called. If a bug is found by a finalize () action, then the problem can be found out based on it - that's what people really care about. Here is a simple example of how finalize() can be used:

// housekeeping/TerminationCondition.java
// Using finalize() to detect a object that
// hasn't been properly cleaned up

import onjava.*;

class Book {
    boolean checkedOut = false;

    Book(boolean checkOut) {
        checkedOut = checkOut;
    }

    void checkIn() {
        checkedOut = false;
    }

    @Override
    protected void finalize() throws Throwable {
        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();
        // Drop the reference, forget to clean up:
        new Book(true);
        // Force garbage collection & finalization:
        System.gc();
        new Nap(1); // One second delay
    }

}

Output:

Error: checked out

The end condition of this example is that all Book objects must be registered before they can be garbage collected. But in the main() method, there is a Book that is not registered. Without the finalize() method to verify the termination condition, it will be difficult to find this bug.

You may have noticed the use of @ Override. @This means that this is an annotation, which is additional information about the code. Here, the annotation tells the compiler that this is not an accidental redefinition of the finalize() method that exists in every object - the programmer knows what he is doing. The compiler ensures that you don't misspell the method name and that the method exists in the base class. Annotation is also a reminder to readers. @ Override was introduced in Java 5 and improved in Java 7. The whole book will appear.

be careful, System.gc() is used to force a termination action. But even if you don't, if you execute the program repeatedly (assuming that the program will allocate a large amount of storage space leading to the execution of garbage collection actions), you can finally find the wrong Book object.

You should always assume that the base class version of finalize() also needs to do something important, using super to call it, just like in Book.finalize As seen in (). In this case, it's commented out because it requires exception handling, which we haven't covered so far.

How the garbage collector works

If you have used a language before, it is very expensive to allocate objects on the heap, and you may naturally feel that all objects in Java (except basic types) are allocated on the heap in a very high way. However, the garbage collector can significantly improve the speed of object creation. This sounds strange - the release of storage space affects the allocation of storage space, but it's really how some Java virtual machines work. This also means that Java can allocate space from the heap as fast as other languages can allocate space on the stack.

For example, you can think of a heap in C + + as a yard in which each object is responsible for managing its own site. After a period of time, the object may be destroyed, but the site must be reused. In some Java virtual machines, the implementation of heap is quite different: it is more like a conveyor belt, and it moves forward one grid for each new object allocated. This means that the allocation of object storage space is very fast. Java's heap pointer simply moves to the unallocated area, so its efficiency is equivalent to that of C + + in allocating space on the stack. Of course, in the actual process, there is a small amount of extra cost in bookkeeping work, but this part of the cost is less than the cost of finding available space.

You may realize that the heap in Java doesn't work exactly like a conveyor belt. In that case, it will inevitably lead to frequent memory paging - moving it in and out of the hard disk, so it will appear that you need more memory than you actually need. Page scheduling can have a significant impact on performance. Finally, after creating enough objects, memory resources are exhausted. The secret is the involvement of the garbage collector. When it works, it reclaims memory and compacts the objects in the heap so that the "heap pointer" can easily move closer to the beginning of the conveyor belt and avoid page errors as much as possible. By rearranging objects, the garbage collector implements a high-speed heap model with infinite space to allocate.

To understand garbage collection in Java, it is helpful to first understand the garbage collection mechanism in other systems. A simple but slow garbage collection mechanism is called reference counting. Each object contains a reference counter. Each time a reference points to the object, the reference counter is increased by 1. When a reference leaves the scope or is set to null, the reference count is decremented by 1. Therefore, managing reference counting is a small overhead but frequent burden in the whole life cycle of a program. The garbage collector traverses the list of all objects. When it finds that the reference count of an object is 0, it frees up the space it occupies (however, the reference count mode often frees the object immediately when the count is 0). This mechanism has a disadvantage: if there are circular references between objects, then their reference count is not 0, there will be a situation that should be recycled but not recycled. For the garbage collector, locating such circular references takes a lot of work. Reference counting is often used to illustrate how garbage collection works, but it never seems to be applied to any Java virtual machine implementation.

In a faster policy, the garbage collector is not based on reference counts. They are based on the fact that for any "live" object, it must eventually be traceable to the reference it survives in the stack or static storage area. This chain of references may pass through several object hierarchies, so if you traverse all references from the stack or static storage area, you will find all "live" objects. For each reference found, it is necessary to track the object it references, and then all references contained in the object, and repeat until the entire network formed by "references rooted in stack or static storage area" is accessed. The object you have visited must be alive. Note that this solves the problem of circular references between objects, which are not discovered and are therefore automatically recycled.

In this way, Java virtual machine adopts an adaptive garbage collection technology. How to deal with the found live objects depends on different Java virtual machine implementations. One is called stop and copy. As the name implies, it needs to pause the program (not belong to the background recycling mode), and then copy all the surviving objects from the current heap to another heap. What is not copied is what needs to be garbage collected. In addition, when objects are copied to the new heap, they are arranged in a compact arrangement next to each other, and then the new space can be allocated simply and directly as described earlier.

When an object is copied from one place to another, all references to it must be fixed. References to stacks or static storage can be fixed directly, but there may be other references to these objects that can only be found during traversal (think of it as a table that maps old addresses to new ones).

This so-called "copy recycler" is inefficient for two main reasons. One: you have to have two heaps, and then you have to toss between the two separate heaps, and you have to maintain twice as much space as you actually need. Some Java virtual machines deal with this problem by allocating several large blocks of memory from the heap on demand, and the copy action occurs between these large blocks of memory.

The second is replication itself. Once the program is in a stable state, it may only generate a small amount of garbage, or even no garbage. However, the copy collector still copies all memory from one place to another, which is wasteful. To avoid this situation, some Java Virtual Opportunities check: if no new garbage is generated, it will switch to another mode (i.e. "adaptive"). This pattern is called Mark and sweep, which has been used by earlier versions of Sun's Java virtual machine. For general use, the mark sweep method is quite slow, but it's fast when you know that the program will generate only a small amount of garbage or even no garbage.

The idea of "tag sweep" is still to start from stack and static storage area, traverse all references, and find out all living objects. However, whenever a live object is found, a tag is set for the object and it is not recycled. The cleanup action only starts when the tagging process is complete. During cleanup, unmarked objects are released and no replication occurs. The remaining heap space after "mark sweep" is discontinuous. If the garbage collector wants continuous space, it needs to rearrange the remaining objects.

"Stop copy" means that this garbage collection action is not performed in the background; on the contrary, when the garbage collection action occurs, the program will pause. It will be found in Oracle's documents that many references regard garbage collection as a low priority background process, but the earlier version of Java virtual machine did not implement garbage collector in this way. When available memory is low, the garbage collector pauses the program. Similarly, the "mark sweep" work can only be carried out when the program is suspended.

As mentioned earlier, in the Java virtual machine discussed here, memory allocation is in large blocks. If the object is large, it takes up a separate block. Strictly speaking, stop copy requires that all living objects must be copied from the old heap to the new heap before releasing the old objects, which results in a large number of memory copy behaviors. With blocks, the garbage collector can copy objects to discarded blocks. Each block has a chronology to record its own survival. Generally, if a block is referenced somewhere and its age is increased by 1, the garbage collector will sort out the newly allocated block after the last recycle action. This is helpful for dealing with a large number of short-lived temporary objects. The garbage collector does a full cleanup on a regular basis - large objects still don't copy (but age increases), and blocks containing small objects are copied and groomed. Java virtual opportunity monitoring, if all objects are stable and the efficiency of garbage collection is reduced, switch to "mark sweep" mode. Similarly, the Java virtual machine keeps track of the "mark sweep" effect. If there are many fragments in the heap space, it will switch back to the "stop copy" mode. This is the origin of "adaptive". You can call it "adaptive, generational, stop copy, mark sweep" garbage collector.

There are many additional technologies in the Java virtual machine to speed up. In particular, technology related to loader operations, known as just in time (JIT) compilers. This technology can translate all or part of the program into local machine code, so no JVM is needed for translation, so it runs faster. When you need to load a class (usually the first object to create it), the compiler finds its. Class file first, and then loads the bytecode of the class into memory. You can let the instant compiler compile all the code, but there are two disadvantages: one is that this loading action runs through the entire program life cycle, and it takes more time to accumulate; the other is that it will increase the length of the executable code (bytecode is much smaller than the local machine code after the instant compiler is expanded), which will lead to page scheduling, thus reducing the program speed . Another approach, called lazy evaluation, means that the immediate compiler compiles code only when necessary. In this way, code that has never been executed may not be JIT compiled at all. The Java HotSpot technology in the new JDK adopts a similar approach. The code is optimized every time it is executed, so the more times it is executed, the faster it will be.

Member initialization

Java tries to ensure that all variables are properly initialized before use. For local variables of methods, this guarantee will be rendered in the wrong way at compile time, so if it is written as:

void f() {
    int i;
    i++;
}

You will get an error message telling you that i may not be initialized. The compiler can assign a default value to i, but uninitialized local variables are more likely to be the negligence of the programmer, so adopting the default value will cover up such mistakes. Forcing the programmer to provide an initial value often helps to find bug s in the program.

If the member variable of a class is a basic type, the situation becomes a little different. As you can see in the "everything is an object" chapter, each basic type data member of a class is guaranteed to have an initial value. The following program can verify such cases and display their values:

// housekeeping/InitialValues.java
// Shows default initial values

public class InitialValues {
    boolean t;
    char c;
    byte b;
    short s;
    int i;
    long l;
    float f;
    double d;
    InitialValues reference;

    void printInitialValues() {
        System.out.println("Data type Initial value");
        System.out.println("boolean " + t);
        System.out.println("char[" + c + "]");
        System.out.println("byte " + b);
        System.out.println("short " + s);
        System.out.println("int " + i);
        System.out.println("long " + l);
        System.out.println("float " + f);
        System.out.println("double " + d);
        System.out.println("reference " + reference);
    }

    public static void main(String[] args) {
        new InitialValues().printInitialValues();
    }
}

Output:

Data type Initial value
boolean false
char[NUL]
byte 0
short 0
int 0
long 0
float 0.0
double 0.0
reference null

It can be seen that although the initial values of data members are not given, they do have initial values (char value is 0, so it is blank). So at least there is no risk of uninitialized variables.

When an object reference is defined in a class, if it is not initialized, the reference will be assigned null.

Specify initialization

How to assign an initial value to a variable? A very direct way is to assign values to class member variables where they are defined. The following code modifies the definition of the member variable of the InitialValues class and provides the initial value directly:

// housekeeping/InitialValues2.java
// Providing explicit initial values

public class InitialValues2 {
    boolean bool = true;
    char ch = 'x';
    byte b = 47;
    short s = 0xff;
    int i = 999;
    long lng = 1;
    float f = 3.14f;
    double d = 3.14159;
}

You can also initialize objects of non basic types in the same way. If Depth is a class, you can create an object and initialize it as follows:

// housekeeping/Measurement.java

class Depth {}

public class Measurement {
    Depth d = new Depth();
    // ...
}

If you try to use d without giving it an initial value, you will get a runtime error that tells you that an exception has occurred (see the "exception" section for details).

You can also provide an initial value by calling a method:

// housekeeping/MethodInit.java

public class MethodInit {
    int i = f();
    
    int f() {
        return 11;
    }
    
}

This method can have parameters, but these parameters cannot be uninitialized class member variables. Therefore, it can be written as follows:

// housekeeping/MethodInit2.java

public class MethodInit2 {
    int i = f();
    int j = g(i);
    
    int f() {
        return 11;
    }
    
    int g(int n) {
        return n * 10;
    }
}

But you can't write like this:

// housekeeping/MethodInit3.java

public class MethodInit3 {
    //- int j = g(i); // Illegal forward reference
    int i = f();

    int f() {
        return 11;
    }

    int g(int n) {
        return n * 10;
    }
}

Obviously, the correctness of these programs depends on the order of initialization, not on the way they are compiled. As a result, the compiler properly warns against forward references.

This way of initialization is simple and intuitive, but there is a limitation: every object of the class InitialValues has the same initial value, sometimes this is what we need, but sometimes it needs more flexibility.

constructor initialization

You can use constructors to initialize, which gives you more flexibility because you can call methods at run time to initialize. However, this does not prevent automatic initialization, which occurs before the constructor is called. Therefore, if you use the following code:

// housekeeping/Counter.java

public class Counter {
    int i;
    
    Counter() {
        i = 7;
    }
    // ...
}

i is first initialized to 0, then to 7. This is true for all basic types and references, including variables whose initial values are explicitly specified at the time of definition. So the compiler doesn't force you to initialize elements somewhere in the constructor or before you use them - initialization is guaranteed. ,

Order of initialization

The order in which variables are defined in a class determines the order in which they are initialized. Even if variable definitions are interspersed between method definitions, they are initialized before any method, including the constructor, is called. For example:

// housekeeping/OrderOfInitialization.java
// Demonstrates initialization order
// When the constructor is called to create a
// Window object, you'll see a message:

class Window {
    Window(int marker) {
        System.out.println("Window(" + marker + ")");
    }
}

class House {
    Window w1 = new Window(1); // Before constructor

    House() {
        // Show that we're in the constructor:
        System.out.println("House()");
        w3 = new Window(33); // Reinitialize w3
    }

    Window w2 = new Window(2); // After constructor

    void f() {
        System.out.println("f()");
    }

    Window w3 = new Window(3); // At end
}

public class OrderOfInitialization {
    public static void main(String[] args) {
        House h = new House();
        h.f(); // Shows that construction is done
    }
}

Output:

Window(1)
Window(2)
Window(3)
House()
Window(33)
f()

In the House class, the definitions of several Window objects are intentionally scattered around to prove that they are all initialized before the constructor or other methods are called. In addition, w3 is reassigned in the constructor.

As can be seen from the output, reference w3 is initialized twice: once before the constructor is called, and once during the constructor call (the first referenced object will be discarded and garbage collected). This may seem inefficient at first glance, but it ensures proper initialization. Imagine what happens if you define an overloaded constructor in which w3 is not initialized and initial value is not given when w3 is defined?

Initialization of static data

No matter how many objects are created, static data only occupies one storage area. The static keyword cannot be applied to local variables, so it can only work on attributes (fields, fields). If a field is a static primitive type and you do not initialize it, it will get the standard initial value of the primitive type. If it is an object reference, its default initial value is null.

If initialized at definition time, static variables look like non static variables.

The following example shows when static storage is initialized:

// housekeeping/StaticInitialization.java
// Specifying initial values in a class definition

class Bowl {
    Bowl(int marker) {
        System.out.println("Bowl(" + marker + ")");
    }
    
    void f1(int marker) {
        System.out.println("f1(" + marker + ")");
    }
}

class Table {
    static Bowl bowl1 = new Bowl(1);
    
    Table() {
        System.out.println("Table()");
        bowl2.f1(1);
    }
    
    void f2(int marker) {
        System.out.println("f2(" + marker + ")");
    }
    
    static Bowl bowl2 = new Bowl(2);
}

class Cupboard {
    Bowl bowl3 = new Bowl(3);
    static Bowl bowl4 = new Bowl(4);
    
    Cupboard() {
        System.out.println("Cupboard()");
        bowl4.f1(2);
    }
    
    void f3(int marker) {
        System.out.println("f3(" + marker + ")");
    }
    
    static Bowl bowl5 = new Bowl(5);
}

public class StaticInitialization {
    public static void main(String[] args) {
        System.out.println("main creating new Cupboard()");
        new Cupboard();
        System.out.println("main creating new Cupboard()");
        new Cupboard();
        table.f2(1);
        cupboard.f3(1);
    }
    
    static Table table = new Table();
    static Cupboard cupboard = new Cupboard();
}

Output:

Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
main creating new Cupboard()
Bowl(3)
Cupboard()
f1(2)
main creating new Cupboard()
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)

The Bowl class shows the creation of classes, while Table and Cupboard contain static data members of the Bowl type in their class definitions. Note that before the static data member is defined, a non-static member b3 of Bowl type is defined in the Cupboard class.

As can be seen from the output, static initialization only takes place when necessary. If you do not create a table object, you do not reference it Table.bowl1 Or Table.bowl2 The static bowl1 and bowl2 objects will never be created. They are initialized only when the first table object is created (or accessed). After that, static objects are not initialized again.

The order of initialization is first static objects (if they were not initialized before), then non static objects, as you can see from the output. To execute the main() method, you must load the StaticInitialization class. Its static properties table and cupboard are then initialized, which will cause their corresponding classes to be loaded. Because they both contain static Bowl objects, the Bowl class will also be loaded. Therefore, in this special program, all classes will be loaded before the main () method. This is usually not the case, because in a typical program, everything is not connected through static as shown in this example.

To summarize the process of creating objects, suppose there is a class named Dog:

  1. Even if you don't explicitly use the static keyword, the constructor is actually a static method. So, when you create an object of type dog for the first time or access a static method or property of a dog class for the first time, the Java interpreter must look in the classpath to locate Dog.class .
  2. After loading Dog.class After (as you'll learn later, this will create a Class object), all actions related to static initialization are performed. Therefore, static initialization is only initialized once when the Class object is first loaded.
  3. When you create an object with new Dog(), you first allocate enough storage space on the heap for the Dog object.
  4. The allocated storage space will be cleared first, that is, all the basic type data in the Dog object will be set to the default value (the number will be set to 0, the boolean type and character type will be the same), and the reference will be set to null.
  5. Perform all initialization actions that appear at the field definition.
  6. Execute the constructor. As you'll see in the reuse chapter, this can involve many actions, especially when it comes to inheritance.

Explicit static initialization

You can place a set of static initialization actions in a special "static clause" (sometimes called a static block) within a class. Like this:

// housekeeping/Spoon.java

public class Spoon {
    static int i;
    
    static {
        i = 47;
    }
}

This looks like a method, but it's actually just a block of code following the static keyword. As with other static initialization actions, this code is executed only once: when an object of this class is created for the first time or when a static member of this class is accessed for the first time (or even when an object of this class does not need to be created). For example:

// housekeeping/ExplicitStatic.java
// Explicit static initialization with "static" clause

class Cup {
    Cup(int marker) {
        System.out.println("Cup(" + marker + ")");
    }
    
    void f(int marker) {
        System.out.println("f(" + marker + ")");
    }
}

class Cups {
    static Cup cup1;
    static Cup cup2;
    
    static {
        cup1 = new Cup(1);
        cup2 = new Cup(2);
    }
    
    Cups() {
        System.out.println("Cups()");
    }
}

public class ExplicitStatic {
    public static void main(String[] args) {
        System.out.println("Inside main()");
        Cups.cup1.f(99); // [1]
    }
    
    // static Cups cups1 = new Cups(); // [2]
    // static Cups cups2 = new Cups(); // [2]
}

Output:

Inside main
Cup(1)
Cup(2)
f(99)

Whether the static cup1 object is accessed through the line marked [1], or the line marked [1] is removed to run the line marked [2] (the comment of [2] is removed), the static initialization action of Cups will be executed. If you annotate [1] and [2] at the same time, the static initialization of Cups will not take place. In addition, if the comments marked as [2] are removed or only one is removed, static initialization will only be performed once.

Non static instance initialization

Java provides a similar syntax called instance initialization, which is used to initialize non-static variables of each object, such as:

// housekeeping/Mugs.java
// Instance initialization

class Mug {
    Mug(int marker) {
        System.out.println("Mug(" + marker + ")");
    }
}

public class Mugs {
    Mug mug1;
    Mug mug2;
    { // [1]
        mug1 = new Mug(1);
        mug2 = new Mug(2);
        System.out.println("mug1 & mug2 initialized");
    }
    
    Mugs() {
        System.out.println("Mugs()");
    }
    
    Mugs(int i) {
        System.out.println("Mugs(int)");
    }
    
    public static void main(String[] args) {
        System.out.println("Inside main()");
        new Mugs();
        System.out.println("new Mugs() completed");
        new Mugs(1);
        System.out.println("new Mugs(1) completed");
    }
}

Output:

Inside main
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs()
new Mugs() completed
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs(int)
new Mugs(1) completed

It looks like a static code block, but it's missing the static keyword. This syntax is necessary to support the initialization of "anonymous inner classes" (see Chapter "inner classes"), but you can also use it to ensure that certain operations will occur, regardless of which constructor is called. As you can see from the output, the instance initialization clause is executed before two constructors.

Array initialization

An array is a sequence of objects or basic type data of the same type encapsulated by an identifier name. Arrays are defined and used by the bracket subscript operator []. To define an array reference, you simply need to enclose the type name with parentheses:

int[] a1;

Square brackets can also be placed after identifiers, meaning the same:

int a1[];

This format conforms to the habits of C and C + + programmers. However, the former format may be more reasonable, after all, it indicates that the type is "an int array". This format is used in this book.

The compiler does not allow specifying the size of an array. This brings us back to the issue of reference. All you have is a reference to the array (you've allocated enough storage space for that reference), but you haven't allocated any space to the array object itself. In order to create the corresponding storage space for an array, an initialization expression must be written. For arrays, the initialization action can appear anywhere in the code, but you can also use a special initialization expression that must appear where the array was created. This particular initialization consists of a pair of values enclosed in curly braces. In this case, the allocation of storage space (equivalent to using new) will be the responsibility of the compiler. For example:

int[] a1 = {1, 2, 3, 4, 5};

So why define an array reference when there is no array?

int[] a2;

In Java, you can assign an array to another array, so you can do this:

a2 = a1;

In fact, what we really do is to copy a reference, as shown below:

// housekeeping/ArraysOfPrimitives.java

public class ArraysOfPrimitives {
    public static void main(String[] args) {
        int[] a1 = {1, 2, 3, 4, 5};
        int[] a2;
        a2 = a1;
        for (int i = 0; i < a2.length; i++) {
            a2[i] += 1;
        }
        for (int i = 0; i < a1.length; i++) {
            System.out.println("a1[" + i + "] = " + a1[i]);
        }
    }
}

Output:

a1[0] = 2;
a1[1] = 3;
a1[2] = 4;
a1[3] = 5;
a1[4] = 6;

a1 is initialized, but a2 is not; here, a2 is assigned to another array later. Since a1 and a2 are aliases of the same array, the changes made through a2 can also be seen in a1.

All arrays (whether it is an object array or an array of basic types) have a fixed member length, which tells you how many elements there are in the array and you cannot modify them. Similar to C and C + +, Java array counting starts from 0, and the maximum number of subscripts that can be used is length - 1. Beyond this boundary, C and C + + will accept by default, allowing you to access all the memory, and many infamous bug s are born from this. But Java will report runtime errors (exceptions) when you access beyond this boundary, so as to avoid such problems.

Dynamic array creation

If you are not sure how many elements you need in the array when you write your program, you can use new to create elements in the array. As shown in the following example, use new to create an array of basic types. New cannot create basic type data other than an array:

// housekeeping/ArrayNew.java
// Creating arrays with new
import java.util.*;

public class ArrayNew {
    public static void main(String[] args) {
        int[] a;
        Random rand = new Random(47);
        a = new int[rand.nextInt(20)];
        System.out.println("length of a = " + a.length);
        System.out.println(Arrays.toString(a));
    } 
}

Output:

length of a = 18
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

The size of the array is through Random.nextInt() determined at random, this method returns a value between 0 and the input parameter. Because of randomness, it's clear that arrays are created at run time. In addition, the program output indicates that the basic data type value in the array element is automatically initialized to the default value (0 for numbers and characters; false for Booleans). Arrays.toString() yes java.util Methods in standard class libraries produce printable versions of one-dimensional arrays.

In this example, the array can also be initialized at the same time of definition:

int[] a = new int[rand.nextInt(20)];

If possible, try to do so.

If you create an array of non basic types, you create a reference array. Take Integer as an example. It is a class rather than a basic type:

// housekeeping/ArrayClassObj.java
// Creating an array of nonprimitive objects

import java.util.*;

public class ArrayClassObj {
    public static void main(String[] args) {
        Random rand = new Random(47);
        Integer[] a = new Integer[rand.nextInt(20)];
        System.out.println("length of a = " + a.length);
        for (int i = 0; i < a.length; i++) {
            a[i] = rand.nextInt(500); // Autoboxing
        }
        System.out.println(Arrays.toString(a));
    }
}

Output:

length of a = 18
[55, 193, 361, 461, 429, 368, 200, 22, 207, 288, 128, 51, 89, 309, 278, 498, 361, 20]

Here, even after creating an array with new:

Integer[] a = new Integer[rand.nextInt(20)];	

It is only a reference array, and initialization is not finished until a new Integer object is created (through auto boxing) and the object is assigned to the reference:

a[i] = rand.nextInt(500);

If you forget to create an object, but try to use an empty reference in the array, an exception will occur at run time.

You can also initialize an array with a list enclosed in curly braces in two forms:

// housekeeping/ArrayInit.java
// Array initialization
import java.util.*;

public class ArrayInit {
    public static void main(String[] args) {
        Integer[] a = {
                1, 2,
                3, // Autoboxing
        };
        Integer[] b = new Integer[] {
                1, 2,
                3, // Autoboxing
        };
        System.out.println(Arrays.toString(a));
        System.out.println(Arrays.toString(b));

    }
}

Output:

[1, 2, 3]
[1, 2, 3]

In both forms, the last comma of the initialization list is optional (this feature makes it easier to maintain long lists).

Although the first form is useful, it is more limited because it can only be used at array definitions. The second and third forms can be used anywhere, even inside a method. For example, you create a String array and pass it to the main() method of another class, as follows:

// housekeeping/DynamicArray.java
// Array initialization

public class DynamicArray {
    public static void main(String[] args) {
        Other.main(new String[] {"fiddle", "de", "dum"});
    }
}

class Other {
    public static void main(String[] args) {
        for (String s: args) {
            System.out.print(s + " ");
        }
    }
}

Output:

fiddle de dum 

Other.main() parameters are created at the call, so you can even provide replaceable parameters at the method call.

Variable parameter list

You can create and call methods with a variable parameter list similar to C (which C usually calls "varargs"). This can be applied when the number or type of parameters is unknown. Since all classes are finally inherited from the Object class (you will have a deeper understanding of this with the development of this book), you can create a method with the Object array as the parameter, and call it as follows:

// housekeeping/VarArgs.java
// Using array syntax to create variable argument lists

class A {}

public class VarArgs {
    static void printArray(Object[] args) {
        for (Object obj: args) {
            System.out.print(obj + " ");
        }
        System.out.println();
    }
    
    public static void main(String[] args) {
        printArray(new Object[] {47, (float) 3.14, 11.11});
        printArray(new Object[] {"one", "two", "three"});
        printArray(new Object[] {new A(), new A(), new A()});
    }
}

Output:

47 3.14 11.11 
one two three 
A@15db9742 A@6d06d69c A@7852e922

The parameter of printArray() is an Object array, which uses for in syntax to traverse and print each item of the array. The standard Java library can output meaningful content, but the Object created here is the class Object, and the printed content is the class name, followed by an @ symbol and multiple hexadecimal numbers. Therefore, the default behavior (if the toString() method is not defined, it will be described later) is to print the class name and the Object address.

You may see code written before Java 5 like the one above that produces variable parameter lists. In Java 5, this long-awaited feature has finally been added, as you can see in printArray():

// housekeeping/NewVarArgs.java
// Using array syntax to create variable argument lists

public class NewVarArgs {
    static void printArray(Object... args) {
        for (Object obj: args) {
            System.out.print(obj + " ");
        }
        System.out.println();
    }
    
    public static void main(String[] args) {
        // Can take individual elements:
        printArray(47, (float) 3.14, 11.11);
        printArray(47, 3.14F, 11.11);
        printArray("one", "two", "three");
        printArray(new A(), new A(), new A());
        // Or an array:
        printArray((Object[]) new Integer[] {1, 2, 3, 4});
        printArray(); // Empty list is OK
    }
}

Output:

47 3.14 11.11 
47 3.14 11.11 
one two three 
A@15db9742 A@6d06d69c A@7852e922 
1 2 3 4 

With variable parameters, you don't have to explicitly write array syntax anymore. When you specify parameters, the compiler will actually fill the array for you. What you get is still an array, which is why printarray () can use for in to iterate over arrays. However, this is more than just an automatic conversion from an element list to an array. Note that in the penultimate line of the program, an Integer array (created by auto boxing) is transformed into an Object array (to remove compiler warnings) and passed to printArray(). Obviously, the compiler will find that this is an array and will not perform the conversion. Therefore, if you have a set of things, you can pass them as a list, and if you already have an array, the method will accept them as a list of variable parameters.

The last line of the program indicates that the number of variable parameters can be 0. This feature helps when you have optional trailing parameters:

// housekeeping/OptionalTrailingArguments.java

public class OptionalTrailingArguments {
    static void f(int required, String... trailing) {
        System.out.print("required: " + required + " ");
        for (String s: trailing) {
            System.out.print(s + " ");
        }
        System.out.println();
    }
    
    public static void main(String[] args) {
        f(1, "one");
        f(2, "two", "three");
        f(0);
    }
}

Output:

required: 1 one 
required: 2 two three 
required: 0 

This program shows how to use a variable parameter list of types other than the Object class. Here, all variable parameters are String objects. You can use any type of parameter in the variable parameter list, including the base type. The following example shows how a variable parameter list becomes an array, and if there is no element in the list, it becomes an array of size 0:

// housekeeping/VarargType.java

public class VarargType {
    static void f(Character... args) {
        System.out.print(args.getClass());
        System.out.println(" length " + args.length);
    }
    
    static void g(int... args) {
        System.out.print(args.getClass());
        System.out.println(" length " + args.length)
    }
    
    public static void main(String[] args) {
        f('a');
        f();
        g(1);
        g();
        System.out.println("int[]: "+ new int[0].getClass());
    }
}

Output:

class [Ljava.lang.Character; length 1
class [Ljava.lang.Character; length 0
class [I length 1
class [I length 0
int[]: class [I

The getClass() method belongs to the Object class, which will be introduced in the chapter "type information". It produces the class of the Object, and when you print the class, you see an encoded string that represents the type of the class. The leading [represents an array of types following, I represents the basic type int; for double checking, I created an array of int in the last line and printed its type. This also verifies that the use of variable parameter lists does not depend on automatic boxing, but on basic types.

However, the variable parameter list and auto boxing can coexist harmoniously as follows:

// housekeeping/AutoboxingVarargs.java

public class AutoboxingVarargs {
    public static void f(Integer... args) {
        for (Integer i: args) {
            System.out.print(i + " ");
        }
        System.out.println();
    }
    
    public static void main(String[] args) {
        f(1, 2);
        f(4, 5, 6, 7, 8, 9);
        f(10, 11, 12);
        
    }
}

Output:

1 2
4 5 6 7 8 9
10 11 12

Note that you can mix types together in a single parameter list, and the auto boxing mechanism selectively promotes int type parameters to Integer.

Variable parameter lists make method overloading more complex, although at first glance it seems safe enough:

// housekeeping/OverloadingVarargs.java

public class OverloadingVarargs {
    static void f(Character... args) {
        System.out.print("first");
        for (Character c: args) {
            System.out.print(" " + c);
        }
        System.out.println();
    }
    
    static void f(Integer... args) {
        System.out.print("second");
        for (Integer i: args) {
            System.out.print(" " + i);
        }
        System.out.println();
    }
    
    static void f(Long... args) {
        System.out.println("third");
    }
    
    public static void main(String[] args) {
        f('a', 'b', 'c');
        f(1);
        f(2, 1);
        f(0);
        f(0L);
        //- f(); // Won's compile -- ambiguous
    }
}

Output:

first a b c
second 1
second 2 1
second 0
third

In each case, the compiler uses automatic boxing to match the overloading method, and then calls the most explicit matching method.

But if you call f() without parameters, the compiler can't know which method to call. Although this error can be made clear, it can surprise client programmers.

You may solve this problem by adding a non variable parameter to a method:

// housekeeping/OverloadingVarargs2.java
// {WillNotCompile}

public class OverloadingVarargs2 {
    static void f(float i, Character... args) {
        System.out.println("first");
    }
    
    static void f(Character... args) {
        System.out.println("second");
    }
    
    public static void main(String[] args) {
        f(1, 'a');
        f('a', 'b');
    }
}

The {will not compile} annotation excludes this file from the book's Gradle build. If you compile it manually, you will get the following error message:

OverloadingVarargs2.java:14:error:reference to f is ambiguous f('a', 'b');
\^
both method f(float, Character...) in OverloadingVarargs2 and method f(Character...) in OverloadingVarargs2 match 1 error

If you add a non variable parameter to both methods, you can solve the problem:

// housekeeping/OverloadingVarargs3

public class OverloadingVarargs3 {
    static void f(float i, Character... args) {
        System.out.println("first");
    }
    
    static void f(char c, Character... args) {
        System.out.println("second");
    }
    
    public static void main(String[] args) {
        f(1, 'a');
        f('a', 'b');
    }
}

Output:

first
second

You should always use variable parameter lists on a version of overloaded methods, or not use them at all.

Enumeration type

Java 5 adds a seemingly small feature enum keyword, which makes it easy to handle when we need groups and use enumeration type sets. Previously, you needed to create an integer constant set, but these values didn't limit themselves to that constant set, so using them was more risky and harder. Enumeration type is a very common requirement, which is already owned by C, C + + and many other languages. Before Java 5, Java programmers had to understand many details and take extra care to achieve the effect of enum. Now Java also has enum, and its functions are much more complete than those in C/C + +. Here is a simple example:

// housekeeping/Spiciness.java

public enum Spiciness {
    NOT, MILD, MEDIUM, HOT, FLAMING
}

Here you create an enumeration type called spice, which has five values. Because instances of enumeration types are constants, they are all represented in uppercase letters according to the naming convention (separated by underscores if there are more than one word in the name).

To use enum, you need to create a reference of this type and assign it to an instance:

// housekeeping/SimpleEnumUse.java

public class SimpleEnumUse {
    public static void main(String[] args) {
        Spiciness howHot = Spiciness.MEDIUM;
        System.out.println(howHot);
    }
}

Output:

MEDIUM

When you create enum, the compiler automatically adds some useful features. For example, it creates the toString() method so that you can easily display the name of an Enum instance, as you can see from the output in the example above. The compiler also creates the ordinal() method to represent the declaration order of a specific enum constant. The static values() method generates an array of these constant values according to the declaration order of enum constant:

// housekeeping/EnumOrder.java

public class EnumOrder {
    public static void main(String[] args) {
        for (Spiciness s: Spiciness.values()) {
            System.out.println(s + ", ordinal " + s.ordinal());
        }
    }
}

Output:

NOT, ordinal 0
MILD, ordinal 1
MEDIUM, ordinal 2
HOT, ordinal 3
FLAMING, ordinal 4

Although enum looks like a new data type, this keyword only generates some compiler behavior when generating enum classes, so to a large extent you can treat enum as any other class. In fact, enum is really a class and has its own methods.

One of the practical features of enum is to use it in switch statements:

// housekeeping/Burrito.java

public class Burrito {
    Spiciness degree;
    
    public Burrito(Spiciness degree) {
        this.degree = degree;
    }
    
    public void describe() {
        System.out.print("This burrito is ");
        switch(degree) {
            case NOT:
                System.out.println("not spicy at all.");
                break;
            case MILD:
            case MEDIUM:
                System.out.println("a little hot.");
                break;
            case HOT:
            case FLAMING:
            default:
                System.out.println("maybe too hot");
        }
    }
    
    public static void main(String[] args) {
        Burrito plain = new Burrito(Spiciness.NOT),
        greenChile = new Burrito(Spiciness.MEDIUM),
        jalapeno = new Burrito(Spiciness.HOT);
        plain.describe();
        greenChile.describe();
        jalapeno.describe();
    }
}

Output:

This burrito is not spicy at all.
This burrito is a little hot.
This burrito is maybe too hot.

Because switch is selected from a limited set of possible values, it is an excellent combination with enum. Notice how the name of enum can more clearly indicate the purpose of the program.

In general, you can use enum as another way to create data types, and then use the resulting types. That's the point, so you don't have to think about them too much. Before enum is introduced, you have to spend a lot of energy to create an equivalent enumeration type, which is safe and available.

These introductions are enough for you to understand and use the basic enum. We will discuss them in more depth in the chapter "enumeration".

Summary of this chapter

Constructors, this seemingly sophisticated initialization mechanism, should give you a strong hint: initialization plays an important role in programming languages. During the design of C + +, Bjarne Stroustrup, the inventor of C + +, found in the initial investigation on the productivity of C language that wrong initialization would lead to a large number of programming errors. These mistakes are hard to find, and so are unreasonable cleanups. Because the constructor guarantees proper initialization and cleanup (the compiler does not allow objects to be created without proper constructor calls), you have complete control and security.

In C + +, destructors are important because objects created with new must be explicitly destroyed. In Java, the garbage collector will automatically release the memory of all objects, so many times similar cleaning methods are not needed (but when you need to use them, you have to do it yourself). When there is no need to behave like an destructor, Java garbage collector greatly simplifies programming and enhances the security of memory management. Some garbage collectors can even clean up other resources, such as drawing and file handles. However, the garbage collector does increase the run-time overhead, and since the Java interpreter is slow from the beginning, it's hard to see how much impact this overhead has had. Over time, Java has improved a lot in terms of performance, but speed is still a barrier to its involvement in some specific programming areas.

Since you want to ensure that all objects are created, constructors are actually more complex than discussed here. Especially when creating a new class through composition or inheritance, this guarantee still holds and requires some additional syntax to support it. In later chapters, you'll learn about composition, inheritance, and how they affect constructors.

Tags: Java Programming jvm less

Posted on Mon, 08 Jun 2020 23:28:22 -0400 by pakmannen