All classes in Java inherit from the java.lang.Object class, which has 11 methods:

public final native Class<? > getClass(); public native inthashCode();

public boolean equals(Object obj) {
    return (this == obj);
}

protected native Object clone() throws CloneNotSupportedException;

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

public final native void notify();

public final native void notifyAll();

public final native void wait(long timeout) throws InterruptedException;

public final void wait(long timeout, int nanos) throws InterruptedException {
    if (timeout < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (nanos < 0 || nanos > 999999) {
        throw new IllegalArgumentException(
                            "nanosecond timeout value out of range");
    }

    if (nanos > 0) {
        timeout++;
    }

    wait(timeout);
}

public final void wait() throws InterruptedException {
    wait(0);
}

protected void finalize() throws Throwable { }
Copy the code

GetClass method

This is a native method and is ‘final’, meaning that this method cannot be overridden in subclasses. The getClass method returns the corresponding Class of the current instance, which means that no matter how many instances there are of a Class, getClass returns the same Class object for each instance. Look at the following example:

Integer i1 = new Integer(1);
Class i1Class = i1.getClass();

Integer i2 = new Integer(1);
Class i2Class = i2.getClass();
System.out.println(i1Class == i2Class);
Copy the code

The above code runs true, meaning that the getClass method of both instances of Integer returns the same Class object.

Integer. The class and int. Class

There is also a Java method for obtaining a Class. For example, if we want to obtain the Class of an Integer instance, we can directly obtain the Class from integer. Class.

Integer num = new Integer(1);
Class numClass = num.getClass();
Class integerClass = Integer.class;
System.out.println(numClass == integerClass);
Copy the code

The above code returns true, which means that calling the instance’s getClass method returns the same class as the.class method. An Integer object also has a native class of type int, corresponding to integer. class, which is used to get a class of type int. But they don’t return the same object. Look at the following example:

Class intClass = int.class;
Class intTYPE = Integer.TYPE;
Class integerClass = Integer.class;
System.out.println(intClass == integerClass);
System.out.println(intClass == intTYPE);
Copy the code

The above code runs as follows:

false
true
Copy the code

Integer.class and int.class do not return the same object, while int.class and integer. TYPE return the same object. Integer.type is defined as follows:

public static final Class<Integer>  TYPE = (Class<Integer>) Class.getPrimitiveClass("int");
Copy the code

In Java primitive types (Boolean, byte, char, short, int, long, float, double) and void created a pre-defined types, can through the wrapper class TYPE static attributes. In the Integer class above, TYPE andint.classIs equivalent.

HashCode methods

Object hash code is mainly used for storing and searching in hash table. The Java specification for object hashCode methods is as follows:

  1. The hashCode method returns the same integer value no matter how many times an object is called during the execution of a Java program, provided that the object is not changed. The hash code of the object does not have to keep the same value in different programs.
  2. If two objects are the same using the equals method, then the hashCode values of the two objects must also be equal.
  3. If two objects are not equal according to equals, then the hashCode values of the two objects need not be different. However, unequal objects with different hashCode values can improve hash table performance.

To understand these three specifications, let’s first look at the process of putting an entry in a HashMap:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false.true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null) index TAB [I] = newNode(hash, key, value, null); // If there is no entry corresponding to the key in the current hash table, it is directly insertedelse{// The value Node<K,V> e of this entry needs to be updated; K k; // Check that the current node is an entry that already exists: 1: hashcode is equal; 2: The key and key of the current node are the same object (==) or their equals method determines equalityif (p.hash == hash&& ((k = p.key) == key || (key ! = null && key.equals(k)))) e = p;else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash&& ((k = e.key) == key || (key ! = null && key.equals(k))))break; p = e; }} // Whether old values need to be updated, and if so, updatedif(e ! = null) { // existing mappingfor key
            V oldValue = e.value;
            if(! onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e);return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
Copy the code

There are several steps to put a key-value pair in a HashMap:

  1. Calculate the hash code of the key and use the hash code to locate the position of the new entry in the hash table. If the current position is null, it indicates that there is no entry corresponding to the key in the hash table. Directly insert a new node.
  2. If the current key has been mapped in the hash table, the node is first searched to determine whether the current node is the target node under two conditions: 1) The hash codes of the two nodes must be equal; 2) They are the same object (== true), or their equals method determines that they are equal.
  3. Determine if old values need to be updated, and update if necessary.

After analyzing the PUT operation of HashMap, let’s look at the three specifications:

  1. The first specification requires that the hashCode method of an object that is called multiple times return equal values. Imagine that if the hashCode method of a key returns a different value each time, then the hash table may be located in different places when put. This creates ambiguity: there are multiple different mappings of the same key in the hash table even though the two keys are the same. This violates the principle that the key of a hash table cannot be repeated.
  2. The second stipulation is also easy to understand: if the equals method of two keys determines that they are equal, then only one key needs to be kept in the hash table. If equals determines equality but HashCode is different, this fact is violated.
  3. The third protocol is used to optimize the performance of hash tables. If hash tables are put with too many “collisions”, lookup performance will inevitably decline.

The equals method

The equals method is used to determine whether two objects are equal. The equals method in Object actually compares whether two objects have the same address by default. That’s the memory semantics for “==”.

But in a subclass we can override equals to determine if two instances of the subclass are the same object. For example, for a person, he has many attributes, but each person’s ID is unique. If two people have the same ID, it proves that two people are the same person, regardless of other attributes. At this point we can override the person’s equals method to ensure this.

public class Person {
    
    private long id;
    private String name;
    private int age;
    private String nation;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if(o == null || getClass() ! = o.getClass())return false;
        Person person = (Person) o;
        returnid == person.id; } @override public int if two persons have the same ID, they are considered the same objecthashCode() {
        returnObjects.hash(id); }}Copy the code

Equals method on non-empty object references:

  1. Reflexive, reflexive. Any non-null reference value x must return true for x.equals(x)
  2. Symmetric C. symmetric D. symmetric Any non-null reference values x and y, if x.evers (y) is true, then y.evers (x) must also be true
  3. Transitive, transitive. For any non-null reference values x, y, and z, if x. quals(y) is true and y.quals (z) is true, then x. quals(z) must also be true
  4. Consistent, consistent. Any non-null reference to values x and y, and multiple calls to x.equals(y) always return true or always return false, provided that the information used in the equals comparison on the object has not been modified
  5. For any non-null reference value x, x.equals(null) should return false

Java requires that both equals and hashCode methods of a class be overridden. We’ve just looked at how keys are handled in a HashMap: first, locate the hash table based on the hash code of the key, and then determine whether two keys are the same based on the “==” or equals method. If Person’s equals method is not overridden, then two Person objects with the same ID will not point to the same memory address, and no existing mapping entry will be found in the hash table.

Clone method

Used to clone an object, cloned objects need to implements the Cloneable interface, otherwise call the object’s clone method, will throw CloneNotSupportedException anomalies. Cloned objects generally comply with the following three rules:

  1. x.clone() ! = x, the cloned object is not the same as the original object, pointing to a different memory address
  2. x.clone().getClass() == x.getClass()
  3. x.clone().equals(x)

When an object is cloned, fields of the native and wrapped types are cloned differently. For primitive types, one is copied directly, while for wrapped types, only a reference is copied, and the reference type itself is not cloned.

Shallow copy

Shallow copy examples:

public class ShallowCopy {

    public static void main(String[] args){
        Man man = new Man();
        Man manShallowCopy = (Man) man.clone();
        System.out.println(man == manShallowCopy);
        System.out.println(man.name == manShallowCopy.name);
        System.out.println(man.mate == manShallowCopy.mate);
    }
}

class People implements Cloneable {

    // primitive type
    public int id;
    // reference type
    public String name;

    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }
}

class Man extends People implements Cloneable {
    // reference type
    public People mate = new People();

    @Override
    public Object clone() {
        returnsuper.clone(); }}Copy the code

The code above runs as follows:

false
true
true
Copy the code

The name and mate attributes of the shallow copy manShallowCopy object refer to the same memory address as the original object Man. When the name and mate of Man are copied, the shallow copy only copies the reference to the same memory address.

Deep copy

When making a deep copy of an object, the properties of the wrapper type of the object will be copied again to achieve the purpose of deep copy, as shown in the following example:

public class DeepCopy {

    public static void main(String[] args){
        Man man = new Man();
        Man manDeepCopy = (Man) man.clone();
        System.out.println(man == manDeepCopy);
        System.out.println(man.mate == manDeepCopy.mate);
    }
}

class People implements Cloneable {

    // primitive type
    public int id;
    // reference type
    public String name;

    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return null;
    }
}

class Man extends People implements Cloneable {
    // reference typepublic People mate = new People(); // Deep copy man@override public Objectclone() { Man man = (Man) super.clone(); man.mate = (People) this.mate.clone(); // apply the mate attribute againcloneTo achieve deep copyreturnman; }}Copy the code

The code above runs as follows:

false
false
Copy the code

In the clone method of Man object, we clone Man first, and then copy the mate attribute. So man’s mate and manDeepCopy’s mate point to different memory addresses. Deep copy.

In general, it is not practical to make a full deep copy of an object. For example, in the above example, we copied the Man attribute mate, but we cannot copy the name (String), which is just a reference.

The toString method

The default toString method in Object is as follows:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
Copy the code

The name of the class + the hash code of the object. Normally in a subclass we can override this method.

Notify method

Notify is a final native method that subclasses are not allowed to override.

The notify method is used to wake up random threads that are waiting for the current object monitor. Generally, notify and WAIT methods are used together to achieve multi-threaded synchronization. After a thread is awakened, it must regain the object’s monitor lock (which is relinquished after a thread calls the object’s WAIT method) before it can continue execution.

A thread in the call of an object before the notify method must get to the object’s monitor (synchronized), otherwise will throw IllegalMonitorStateException anomalies. Similarly, a thread must obtain the monitor for an object before calling its wait method.

Examples of wait and notify:

Object lock = new Object();
Thread t1 = new Thread(() -> {
    synchronized (lock) {
        System.out.println(Thread.currentThread().getName() + " is going to wait on lock's monitor");
        try {
            lock.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + " relinquishes the lock's monitor"); }}); Thread t2 = new Thread(() -> { synchronized (lock) { System.out.println(Thread.currentThread().getName() +" is going to notify a thread that waits on lock's monitor"); lock.notify(); }}); t1.start(); t2.start();Copy the code

NotifyAll method

NotifyAll is used to wake up all the threads waiting for the monitor lock on an object, and notify only wakes up one of the waiting threads.

Similarly, if the current thread is not the owner of the object’s monitor, then the notifyAll IllegalMonitorStateException abnormal happens.

Wait method

public final native void wait(long timeout) throws InterruptedException;
Copy the code

The wait method is usually used in conjunction with notify. After a thread calls the wait method of an object, the thread enters the WAITING state or TIMED_WAITING state. Until another thread wakes it up.

Thread must wait in the call object method before access to the object of the monitor lock, otherwise will throw IllegalMonitorStateException anomalies. Threads are interrupt-enabled, and if a thread is interrupted by another thread while waiting, InterruptedException is thrown.

If timeout in the wait method is 0, the wait will not timeout until notify or interrupt another thread. If timeout is greater than 0, wait times are supported, after which the thread is automatically woken up.

The finalize method

protected void finalize() throws Throwable { }
Copy the code

When garbage collector collects a garbage object, we will call the Finalize method of the object. We can overwrite the Finalize method of the object to do some cleaning work. Here is an example of Finalize:

public class FinalizeExample {

    public static void main(String[] args) {
        WeakReference<FinalizeExample> weakReference = new WeakReference<>(new FinalizeExample());
        weakReference.get();
        System.gc();
        System.out.println(weakReference.get() == null);
    }

    @Override
    public void finalize() {
        System.out.println("I'm finalized!"); }}Copy the code

Output result:

I'm finalized!
true
Copy the code

Objects in the Finalize method can “save themselves” from being collected by garbage collector. Avoid GC by establishing a strong reference to this in the Finalize method:

public class FinalizeExample {

    private static FinalizeExample saveHook;

    public static void main(String[] args) {
        FinalizeExample.saveHook = new FinalizeExample();
        saveHook = null;
        System.gc();
        System.out.println(saveHook == null);
    }

    @Override
    public void finalize() {
        System.out.println("I'm finalized!"); saveHook = this; }}Copy the code

Output result:

I'm finalized!
false
Copy the code