Creating objects in Java

Posted by faswad on Mon, 03 Jan 2022 14:24:41 +0100

1. new an object

Creating objects with the keyword new is almost one of the most common operations when writing code, such as:

Sheep sheep1 = new Sheep();
Sheep sheep2 = new Sheep( "codesheep", 18, 65.0f );

Through the new method, we can call the parameterless or parameterless constructor of the class to instantiate an object.

On the surface, there are simply new objects, but if you only answer this level during the interview, you will probably jump into the street, because more important than this is the principle and process of new objects, because the matchmaker JVM has silently done a lot of work for us.

When it comes to the specific process of a new object, it can be roughly described as follows with a diagram:

  1. First, when we create a new object, such as sheet sheet = new sheet(), the JVM will first go back and check whether the class represented by the symbolic reference of sheet has been loaded. If not, the loading process of the corresponding class will be executed;
  2. Declaring a type reference is very simple. For example, sheet = new sheet() will declare a reference sheet of sheet type;
  3. In the first step, after the class is loaded, the memory required by the object has been determined, and then the JVM will allocate memory for the object on the heap;
  4. The so-called attribute "0" value initialization is very easy to understand, that is, each attribute of the instantiated object is assigned a default initialization "0" value. For example, the initialization 0 value of int is 0, and the initialization 0 value of an object is null;
  5. Next, the JVM will set the object header, which mainly includes the object's runtime data (such as Hash code, generation age, lock status flag, lock pointer, biased thread ID, biased timestamp, etc.) and type pointer (the JVM determines which class instance the object is through this type pointer);
  6. The display and initialization of attributes are easy to understand. For example, when defining a class, manually assign a value to an attribute field, such as: private String name = "codesheet"; Just at this time to initialize;
  7. Finally, call the constructor of the class to perform the initialization action described in the constructor.

It should be said that after this series of steps, a new available object can be born.

2. Reflect an object

Anyone who has studied Java reflection mechanism knows that as long as you can get the Class object of the Class, you can create an instance object through a powerful reflection mechanism.

Generally speaking, there are three ways to get Class objects:

  • Class name class
  • Object name getClass()
  • Class. Forname (fully qualified class name)

After you have a Class object, you can call its newInstance() method to create an object, like this:

Sheep sheep3 = (Sheep) Class.forName( "cn.codesheep.article.obj.Sheep" ).newInstance();
Sheep sheep4 = Sheep.class.newInstance();

Of course, the limitations of this method are obvious to all, because it uses the nonparametric construction method of class to create objects.

So a further way than this is through Java lang.relect. Constructor uses the newInstance() method of this class to create objects, because it can explicitly specify a constructor to create objects.

For example, after we get the Class object of the Class, we can get the list of all constructors of the Class through the getdeclaraedconstructors() function, so that we can call the corresponding constructor to create the object, like this:

Constructor<?>[] constructors = Sheep.class.getDeclaredConstructors();
Sheep sheep5 = (Sheep) constructors[0].newInstance(); 
Sheep sheep6 = (Sheep) constructors[1].newInstance( "codesheep", 18, 65.1f );

Moreover, if we want to explicitly obtain a constructor of a class, we can also directly specify the constructor parameter type in the getDeclaredConstructors() function to precisely control it, like this:

Constructor constructor = Sheep.class.getDeclaredConstructor( String.class, Integer.class, Float.class );
Sheep sheep7 = (Sheep) constructor.newInstance( "codesheep", 18, 65.2f );

3. Clone an object (and copy knowledge supplement)

Object cloning is basically a rigid requirement when we write code. Cloning another object based on one object is also a very common operation when writing Java code.

3.1 value type and reference type

The accurate distinction between these two concepts is very important for the understanding of deep and shallow copy problems.

So in the Java world, we should get used to using references to manipulate objects. In Java, array, Class, Enum enum, Integer wrapper Class, etc. are typical reference types, so generally speaking, reference passing is also used in operation;

However, the language level basic data types of Java, such as int, generally adopt the method of value transfer during operation, so it is sometimes called value type.

In order to facilitate the following description and examples, we first define two classes: Student and Major, which respectively represent "Student" and "Major learned". The two are inclusive:

// Students' major
public class Major {
    private String majorName; // Professional name
    private long majorId;     // Professional code
    
    // ...  Other omissions
}
// student
public class Student {
    private String name;  // full name
    private int age;      // Age
    private Major major;  // Major studied
    
    // ...  Other omissions
}

3.2. Assignment vs Light copy vs deep copy

Object Assignment

Assignment is the most common operation in daily programming, such as:

Student codeSheep = new Student();
Student codePig = codeSheep;

Strictly speaking, this cannot be regarded as an object copy, because it only copies the reference relationship and does not generate a new actual object:

Shallow copy

Shallow copy is a method of object cloning, and its important characteristic is reflected in the word "shallow".

For example, we try to copy student2 through studen1 instance. If it is a shallow copy, the general model can be shown as follows:

Obviously, the field of value type will be copied, while the field of reference type will only copy the reference address, and the actual object space pointed to by the reference address is actually only one copy.

A picture wins the preface. I think the above picture has been shown very clearly.

Shallow copy code implementation

In the above example, I want to copy student2 through student1. The typical implementation of shallow copy is to let the class of the copied object implement the clonable interface and rewrite the clone() method.

Take the Student class copy above as an example:

public class Student implements Cloneable {
    private String name;  // full name
    private int age;      // Age
    private Major major;  // Major studied
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    // ...  Other omissions
}

Then we write a test code to know:

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Major m = new Major("Computer science and technology",666666);
        Student student1 = new Student( "CodeSheep", 18, m );
        // student2 is copied from student1
        Student student2 = (Student) student1.clone();
        System.out.println( student1 == student2 );
        System.out.println( student1 );
        System.out.println( student2 );
        System.out.println( "\n" );
        // Modify the value type field of student1
        student1.setAge( 35 );
        // Modify the reference type field of student1
        m.setMajorName( "electronic information engineering" );
        m.setMajorId( 888888 );
        System.out.println( student1 );
        System.out.println( student2 );
    }
}

The results are as follows:

The results show that:

  • student1==student2 prints false, indicating that the clone() method has indeed cloned a new object;
  • Modifying the value type field does not affect the cloned new object, which is expected;
  • The reference object inside student1 is modified, and the cloned object student2 is also affected, indicating that it is still associated internally

Deep copy

Compared with the shallow copy shown above, a copy of the value type field will be copied, and a copy of the object pointed to by the reference type field will also be created in memory, just like this:

The principle is very clear. Let's take a look at the specific code implementation.

Deep copy code implementation

Deep traversal copy

Although the clone() method can copy objects, note that the clone() method defaults to shallow copy behavior, just like the above example. If you want to implement deep copy, you need to override the clone() method to implement the deep traversal copy of the reference object and conduct carpet search.

Therefore, compared with the example of shallow copy code implementation, if you want to implement deep copy, you first need to transform the deeper reference class Major to also implement the clonable interface and rewrite the clone() method:

public class Major implements Cloneable {
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    // ...  Other omissions
}

Secondly, we also need to override the clone method in the top-level calling class to call the clone() method of the reference type field to realize deep copy. Corresponding to this article, that is the Student class:

public class Student implements Cloneable {
    @Override
    public Object clone() throws CloneNotSupportedException {
        Student student = (Student) super.clone();
        student.major = (Major) major.clone(); // Important!!!
        return student;
    }
    // ...  Other omissions
}

At this time, the above test cases remain unchanged, and the results can be obtained by running:

Obviously, at this time, student1 and student2 are completely independent from each other.

Deep copy using deserialization

Using deserialization technology, we can also deep copy one object from another, and this product has a surprisingly good effect in solving the problem of multi-layer doll deep copy.

So let's transform the Student class and let its clone() method generate a deep copy of the original object through serialization and deserialization:

public class Student implements Serializable {
    private String name;  // full name
    private int age;      // Age
    private Major major;  // Major studied
    public Student clone() {
        try {
            // Serialize the object itself into a byte stream
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            ObjectOutputStream objectOutputStream =
                    new ObjectOutputStream( byteArrayOutputStream );
            objectOutputStream.writeObject( this );
            // Then the byte stream is deserialized to obtain the object copy
            ObjectInputStream objectInputStream =
                    new ObjectInputStream( new ByteArrayInputStream( byteArrayOutputStream.toByteArray() ) );
            return (Student) objectInputStream.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }
    // ...  Other omissions
}

Of course, in this case, the referenced subclass (such as the Major class here) must also be Serializable, that is, it implements the Serializable interface:

public class Major implements Serializable {
  // ...  Other omissions
}

At this time, the test case is completely unchanged. If you run it directly, you can also get the following results:

Obviously, at this time, student1 and student2 are completely independent, free from mutual interference, and the deep copy is completed.

4. Deserialize an object (and supplement serialization knowledge)

4.1 what is serialization for?

The original intention of serialization is to "transform" a Java object into a byte sequence, so as to facilitate persistent storage to disk and prevent the object from disappearing from memory after the program runs. In addition, transforming it into a byte sequence is also more convenient for network transportation and propagation, so it is conceptually well understood:

  • Serialization: converts Java objects into byte sequences.
  • Deserialization: restores the byte sequence to the original Java object.


In a sense, the serialization mechanism also makes up for some differences in platform. After all, the converted byte stream can be deserialized on other platforms to recover objects.

The thing is just that. It looks very simple, but there are still a lot of things behind. Please look down.

4.2 how to serialize objects?

However, Java currently does not have a keyword to directly define a so-called "persistent" object.

The persistence and de persistence of objects need to rely on the programmer's manual and explicit serialization and de serialization restoration in the code.

For example, suppose we want to serialize the Student class object to a class named Student Txt, and then deserialize it into a Student class object through the text file:

1. Student class definition

public class Student implements Serializable {
    private String name;
    private Integer age;
    private Integer score;
    @Override
    public String toString() {
        return "Student:" + '\n' +
        "name = " + this.name + '\n' +
        "age = " + this.age + '\n' +
        "score = " + this.score + '\n'
        ;
    }
    // ...  Other omissions
}

2. Serialization

public static void serialize(  ) throws IOException {
    Student student = new Student();
    student.setName("CodeSheep");
    student.setAge( 18 );
    student.setScore( 1000 );
    ObjectOutputStream objectOutputStream = 
        new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
    objectOutputStream.writeObject( student );
    objectOutputStream.close();
    System.out.println("Serialization succeeded! Already generated student.txt file");
    System.out.println("==============================================");
}

3. Deserialization

public static void deserialize(  ) throws IOException, ClassNotFoundException {
    ObjectInputStream objectInputStream = 
        new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
    Student student = (Student) objectInputStream.readObject();
    objectInputStream.close();
    System.out.println("The deserialization result is:");
    System.out.println( student );
}

4. Operation results

Console printing:

Serialization succeeded! Already generated student.txt file
==============================================
The deserialization result is:
Student:
name = CodeSheep
age = 18
score = 1000

4.3 what is the use of serializable interface?

When defining the Student class above, we implemented a Serializable interface. However, when we click inside the Serializable interface, we find that it is an empty interface and does not contain any methods!

Imagine what happens if you forget to add implements Serializable when defining the Student class above?

The experimental result is that the program will report an error and throw a NotSerializableException:

We followed the error prompt from the source code to the bottom of the writeObject0() method of ObjectOutputStream, and then we suddenly realized:

If an object is not a string, array or enumeration, and does not implement the Serializable interface, a NotSerializableException will be thrown during serialization!

Oh, I see!

The original Serializable interface is only used as a tag!!!

It tells the code that any class that implements the Serializable interface can be serialized! However, the real serialization action does not need to be completed by it.

4.4 what is the use of serialVersionUID number?

I'm sure you will often see the following code lines defined in some classes, that is, a field named serialVersionUID is defined:

private static final long serialVersionUID = -4392658638228508589L;

Do you know the meaning of this statement? Why do you want a serial number called serialVersionUID?

Let's continue to do a simple experiment. Take the Student class above as an example. We don't explicitly declare a serialVersionUID field in it.

We first call the above serialize() method to serialize a Student object to the Student on the local disk Txt file:

public static void serialize() throws IOException {
    Student student = new Student();
    student.setName("CodeSheep");
    student.setAge( 18 );
    student.setScore( 100 );
    ObjectOutputStream objectOutputStream = 
        new ObjectOutputStream( new FileOutputStream( new File("student.txt") ) );
    objectOutputStream.writeObject( student );
    objectOutputStream.close();
}

Next, let's do something in the Student class. For example, add a field named studentID to indicate the Student number:

At this time, we take the Student that has just been serialized to the local Txt file, and deserialize it with the following code to try to restore the Student object just now:

public static void deserialize(  ) throws IOException, ClassNotFoundException {
    ObjectInputStream objectInputStream = 
        new ObjectInputStream( new FileInputStream( new File("student.txt") ) );
    Student student = (Student) objectInputStream.readObject();
    objectInputStream.close();
    System.out.println("The deserialization result is:");
    System.out.println( student );
}

The runtime found an error and threw an InvalidClassException:

The information prompted here is very clear: the serialVersionUID numbers before and after serialization are incompatible!

At least two important messages can be drawn from this place:

  • 1. serialVersionUID is a unique identifier before and after serialization
  • 2. By default, if no serialVersionUID has been explicitly defined, the compiler will automatically declare one for it!

Question 1: serialVersionUID serialization ID can be regarded as a "secret code" in the process of serialization and deserialization. During deserialization, the JVM will compare the serial number ID in the byte stream with the serial number ID in the serialized class. Only when the two are consistent can it be reordered, otherwise an exception will be reported to terminate the deserialization process.

Question 2: if no one explicitly defines a serialVersionUID when defining a serializable class, the Java runtime environment will automatically generate a default serialVersionUID for the class according to all aspects of the class information. Once the class structure or information is changed as above, the serialVersionUID of the class will also change!

Therefore, for the certainty of serialVersionUID, it is recommended to explicitly declare a serialVersionUID explicit value for all implements Serializable classes when writing code!

Of course, if you don't want to assign values manually, you can also use the automatic addition function of the IDE. For example, I use IntelliJ IDEA. Press alt + enter to automatically generate and add the serialVersionUID field for the class, which is very convenient:

4.5 two special cases

  • 1. Fields modified by static will not be serialized
  • 2. Fields modified by the transient modifier will not be serialized

For the first point, because serialization saves the state of the object rather than the state of the class, it is natural to ignore the static field.

For the second point, we need to understand the role of the transient modifier.

If you do not want a field to be serialized when serializing an object of a class (for example, this field stores privacy values, such as passwords, etc.), you can modify the field with the transient modifier.

For example, if a password field is added to the previously defined Student class, but you do not want to serialize it to txt text, you can:

In this way, when serializing the Student class object, the password field will be set to the default value of null, which can be seen from the results of deserialization:

4.6 control and enhancement of serialization

Binding blessing

From the above process, we can see that there are loopholes in the process of serialization and deserialization, because there is an intermediate process from serialization to deserialization. If someone gets the intermediate byte stream and forges or tampers with it, the deserialized object will have a certain risk.

After all, deserialization is also equivalent to an "implicit" object construction, so we want to perform controlled object deserialization during deserialization.

How about a controlled method?

The answer is: write your own readObject() function for the deserialization construction of objects to provide constraints.

Since you write the readObject() function yourself, you can do many controllable things: such as various judgment work.

Also take the Student class above as an example. Generally speaking, students' scores should be between 0 and 100. In order to prevent students' test scores from being tampered with into a wonderful value by others during deserialization, we can write our own readObject() function for deserialization control:

private void readObject( ObjectInputStream objectInputStream ) throws IOException, ClassNotFoundException {
    // Call the default deserialization function
    objectInputStream.defaultReadObject();
    // Manually check the validity of students' scores after deserialization. If any problem is found, terminate the operation!
    if( 0 > score || 100 < score ) {
        throw new IllegalArgumentException("Student scores can only be between 0 and 100!");
    }
}

For example, I deliberately change the student's score to 101. At this time, the deserialization is terminated immediately and an error is reported:

For the above code, some friends may wonder why the custom private readObject() method can be called automatically. This requires you to follow the underlying source code to explore. I helped you follow the bottom layer of ObjectStreamClass class. I'm sure you will suddenly understand:

It's the reflection mechanism at work again! Yes, in Java, sure enough, everything can be "reflected" (funny). Even the private methods defined in the class can be pulled out and executed, which is really comfortable.

Singleton mode enhancement

An easily overlooked problem is that serializable singleton classes may not be singleton!

A small code example is clear.

For example, here we first write a common singleton mode implementation of "static internal class" in java:

public class Singleton implements Serializable {
    private static final long serialVersionUID = -1576643344804979563L;
    private Singleton() {
    }
    private static class SingletonHolder {
        private static final Singleton singleton = new Singleton();
    }
    public static synchronized Singleton getSingleton() {
        return SingletonHolder.singleton;
    }
}

Then write a validation main function:

public class Test2 {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        ObjectOutputStream objectOutputStream =
                new ObjectOutputStream(
                    new FileOutputStream( new File("singleton.txt") )
                );
        // Serialize the singleton object to the text file singleton Txt
        objectOutputStream.writeObject( Singleton.getSingleton() );
        objectOutputStream.close();
        ObjectInputStream objectInputStream =
                new ObjectInputStream(
                    new FileInputStream( new File("singleton.txt") )
                );
        // The text file singleton The object in txt is deserialized to singleton1
        Singleton singleton1 = (Singleton) objectInputStream.readObject();
        objectInputStream.close();
        Singleton singleton2 = Singleton.getSingleton();
        // The running result actually prints false!
        System.out.println( singleton1 == singleton2 );
    }
}

After running, we found that the deserialized singleton object is not equal to the original singleton object, which undoubtedly does not achieve our goal.

The solution is to write the readResolve() function in the singleton class and directly return the singleton object to avoid it:

private Object readResolve() {
    return SingletonHolder.singleton;
}


In this way, when deserializing an object read from the stream, readResolve() is called to replace the newly deserialized object with the object returned from it.

5. Unsafe black magic

The name of Unsafe class is a little hanging. Indeed, we don't seem to have much contact with business code at ordinary times.

We all know that when writing Java code, we rarely operate some resources at the bottom, such as memory. And in sun misc. The Unsafe class under the Unsafe package path provides a way and method to directly access system resources, and can perform some underlying operations. For example, with Unsafe, we can allocate memory, create objects, free memory, locate the memory location of a field of an object, and even modify it.

It can be seen that this device has great destructive power when misused, so it is generally used under control. It is rarely seen in the business code, but there are still many figures about it in the code in some packages such as io, nio and juc inside the JDK.

The Unsafe class has an allocateInstance() method through which you can create an object. Therefore, we only need to get an instance object of Unsafe class, and we can naturally call allocateInstance() to create the object.

How can I get an instance object of the Unsafe class?

Taking a general look at the source code of Unsafe class, we will find that it is a singleton class, and its construction method is private, so direct construction is not realistic:

public final class Unsafe {
    private static final Unsafe theUnsafe;
    // ...  Omit
    private static native void registerNatives();
    private Unsafe() {
    }
    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }
    // ...  Omit
}

In addition, the entry function getUnsafe() to obtain the singleton object is also marked with a special mark, which means that this method can only be called from the class loaded from the boot, which means that this method is also used internally by the JVM. If it is used directly by external code, exceptions like this will be reported:

Exception in thread "main" java.lang.SecurityException: Unsafe

We have no choice but to regain the powerful reflection mechanism to create an instance of Unsafe class again:

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

Then we can happily use it to create objects:

Sheep sheep8 = (Sheep) unsafe.allocateInstance( Sheep.class );

6. Implicit creation of objects

Of course, in addition to the above explicit object creation scenes, there are also some implicit scenes where we do not create objects manually, for example.

Class instances are created implicitly

We all know that when the JVM virtual machine loads a Class, it will also create a Class instance object corresponding to the Class. Obviously, the JVM secretly does this process behind our back.

String implicit object creation

Typically, for example, when defining a literal variable of String type, it may cause the creation of a new String object, like this:

String name = "codesheep";

It is also common that the + connector of a String will implicitly lead to the creation of a new String object:

String str = str1 + str2;
Automatic packing mechanism

There are also many examples, such as when executing code similar to the following:

Integer codeSheepAge = 18;

The auto boxing mechanism triggered by it will cause a new wrapper type object to be created implicitly in the background.

Function variable parameter

For example, as follows, when we use the variable parameter syntax ` int... Num to describe the input parameters of a function:

public double avg( int... nums ) {
    double sum = 0;
    int length = nums.length;
    for (int i = 0; i<length; ++i) {
        sum += nums[i];
    }
    return sum/length;
}

On the surface, various discrete parameters can be passed in at the call of the function to participate in the calculation:

avg( 2, 2, 4 );
avg( 2, 2, 4, 4 );
avg( 2, 2, 4, 4, 5, 6 );

Secretly, a corresponding array object may be implicitly generated for calculation.

In short, the implicit creation of objects in many scenes is not uncommon. We should at least have a general idea in mind.

Topics: Java Back-end