Java 基础夯实 --- Java 对象的创建

摘要:这篇博文总结一下 Java 当中实例化对象的方法和过程。并且涉及到多态特性在 Java 对象初始化过程中的表现

[toc]

Java 中创建对象的几种方法

  • 通过构造函数

  • 通过 Class 的 newInstace 方法(反射机制)

  • 通过 Constructor 的 newInstance 方法(反射机制)

  • 通过 clone 方法

  • 通过序列化和反序列化机制

注意:

  1. Class 的 newInstance 方法只能通过反射调用对象的 public 的无惨构造函数,而 Constructor 的 newInstance 方法就强大得多,它不仅可以反射调用对象的带参构造方法,也可以调用其 private 构造方法。事实上Class的newInstance方法内部调用的也是Constructor的newInstance方法。

  2. 要想使用clone方法,我们就必须先实现Cloneable接口并实现其定义的clone方法,用clone方法创建对象的过程中并不会调用任何构造函数。

  3. 当我们反序列化一个对象时,JVM会给我们创建一个单独的对象,在此过程中,JVM并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.io.Serializable;
/**
* @author: fanyuzeng
* @date: 2018/3/10 10:39
*/
class Student implements Cloneable,Serializable {
private String mName;
private int mAge;
public Student(String name) {
mName = name;
}
public Student() {
}
private Student(String name, int age) {
mName = name;
mAge = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public String printStudentInfo() {
return "Student{" +
"mName='" + mName + '\'' +
", mAge=" + mAge +
'}';
}
}

注意三个构造方法的作用域以及参数。

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public static void main(String[] args) {
try {
Utils.print("============1.调用构造方法函数创建对象=============");
Student student1 = new Student();
Utils.println("student1:" + student1);
Utils.print("============2.调用Class类的newInstance方法,调用无参数的public的构造方法,创建对象=============");
Student student2 = (Student) Class.forName("Student").newInstance();
Utils.print("student2:" + student2.printStudentInfo());
Student student22 = Student.class.newInstance();
Utils.println("student22:" + student22.printStudentInfo());
Utils.print("============3.调用Constructor类的newInstance方法,调用带参数的public构造方法,创建对象=============");
Constructor<Student> constructor = Student.class.getConstructor(String.class);
Student student3 = constructor.newInstance("WalkerZeng");
Utils.println("student3:" + student3.printStudentInfo());
Utils.print("============4.调用Constructor类的newInstance方法,调用带参数的private构造方法,创建对象=============");
Class<?> aStudent = Class.forName("Student");
Constructor<?> declaredConstructor = aStudent.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
Student student4 = (Student) declaredConstructor.newInstance("WalkerZeng", 24);
Utils.print("student4:" + student4.printStudentInfo());
Utils.println("student4:" + student4);
Utils.print("============5.调用clone方法创建对象=============");
Student student5 = (Student) student4.clone();
Utils.print("student5:" + student5.printStudentInfo());
Utils.println("student5:" + student5);
Utils.print("============6.使用序列化机制创建对象=============");
Constructor<Student> constructor1 = Student.class
.getConstructor(String.class);
Student student6 = constructor1.newInstance("WalkerWang");
Utils.print("student6:" + student6);
Utils.print("student6:" + student6.printStudentInfo());
//写对象
ObjectOutputStream output = new ObjectOutputStream(
new FileOutputStream("student.bin"));
output.writeObject(student6);
output.close();
//读对象
ObjectInputStream input = new ObjectInputStream(new FileInputStream(
"student.bin"));
Student student7 = (Student) input.readObject();
Utils.print("student7:" + student7);
Utils.print("student7:" + student7.printStudentInfo());
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException | NoSuchMethodException | InvocationTargetException | CloneNotSupportedException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

输出情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
============1.调用构造方法函数创建对象=============
student1:Student@140e19d
============2.调用Class类的newInstance方法,调用无参数的public的构造方法,创建对象=============
student2:Student{mName='null', mAge=0}
student22:Student{mName='null', mAge=0}
============3.调用Constructor类的newInstance方法,调用带参数的public构造方法,创建对象=============
student3:Student{mName='WalkerZeng', mAge=0}
============4.调用Constructor类的newInstance方法,调用带参数的private构造方法,创建对象=============
student4:Student{mName='WalkerZeng', mAge=24}
student4:Student@17327b6
============5.调用clone方法创建对象=============
student5:Student{mName='WalkerZeng', mAge=24}
student5:Student@14ae5a5
============6.使用序列化机制创建对象=============
student6:Student@131245a
student6:Student{mName='WalkerWang', mAge=0}
student7:Student@1d6c5e0
student7:Student{mName='WalkerWang', mAge=0}

简单说明几点:

  1. student4 是通过反射调用 Studet 带两个参数的 private 方法创建的。
  2. student5 是通过 student4 clone 创建的,通过观察 student4 和 student5 两者的内存地址不同可知,student5 是一个新的对象。
  3. student7 是通过 student6 反序列化创建的,同样观察两个对象的内存地址可知,student7 是一个新的对象。

Java 对象的创建过程

流程如下图:

实例变量初始化与实例代码块初始化

当申明一个对象的时候,可以同时对其进行赋值,这就是实例变量初始化,也可以使用代码块为其赋值。那么这些操作是在构造函数执行之前完成的。换言之,实例变量初始化和构造函数初始化的代码,实际上是被放在构造函数中,对超类构造函数调用代码之后,构造函数本身的代码之前的。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* @author: fanyuzeng
* @date: 2018/3/10 11:45
*/
class Test1 {
private int i = 1;
private int j = i + 1;
public Test1(int var) {
//此处省略了 super() 超类的无参构造方法是可以省略不写出来的,但是有参构造方法必须写出
Utils.print("i=" + i);
Utils.print("j=" + j);
this.i = var;
Utils.print("i=" + i);
Utils.print("j=" + j);
}
{
j += 4;
}
public static void main(String[] args) {
Test1 test1 = new Test1(3);
}
}
//output:
i=1
j=6
i=3
j=6

根据上面标红的那句话,那么这段代码实际的执行顺序是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @author: fanyuzeng
* @date: 2018/3/10 11:45
*/
class Test1 {
public Test1(int var) {
//此处省略了 super() 超类的无参构造方法是可以省略不写出来的,但是有参构造方法必须写出
private int i = 1;
private int j = i + 1;
{
j += 4;
}
Utils.print("i=" + i);
Utils.print("j=" + j);
this.i = var;
Utils.print("i=" + i);
Utils.print("j=" + j);
}
public static void main(String[] args) {
Test1 test1 = new Test1(3);
}
}

那么,这种在看上面的输出结果,就很显而易见了。这个例子也就验证了上面标红的结论

JVM先会给对象赋默认值

上图中的第二步,JVM 给对象赋默认值,然后再按照代码的赋值情况,去给对象重新赋值。那么赋默认值这个过程到底是否存在呢?我们用代码来验证下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* @author: fanyuzeng
* @date: 2018/3/10 11:57
*/
class Test2 {
private int j = getI();
private int i = 1;
public Test2() {
i = 2;
}
private int getI() {
Utils.print("getI() j="+j+" i="+i);
return i;
}
public static void main(String[] args) {
Test2 test2 = new Test2();
Utils.print(test2.j);
}
}

按照之前的结论,这段代码是可以等价成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @author: fanyuzeng
* @date: 2018/3/10 11:57
*/
class Test2 {
public Test2() {
private int j = getI();
private int i = 1;
i = 2;
}
private int getI() {
Utils.print("getI() j="+j+" i="+i);
return i;
}
public static void main(String[] args) {
Test2 test2 = new Test2();
Utils.print(test2.j);
}
}
//output:
getI() j=0 i=0
0

为什么会输出 0 ? 分析一下,首先调用 Test2 的构造函数,那么就会去给 int 类型的 j 和 i 对象分配内存空间,并且赋默认值,int 类型的默认值是 0 。然后再按照代码的情况去给他们赋值,第 7 行是给 j 赋值的,这个值是 getI() 函数的返回值,那么来看看 getI() 函数,这个函数就是返回 i ,此时的 i 的值是 JVM 赋予的默认值,也就是 0 ,所以这么一来, 就是将 0 赋给 j。

所以这段代码是可以体现出 JVM 在申请内存空间的同时,是对变量进行了一个赋默认值的过程的。

构造函数初始化

我们可以从上文知道,实例变量初始化与实例代码块初始化总是发生在构造函数初始化之前,那么我们下面着重看看构造函数初始化过程。众所周知,每一个Java中的对象都至少会有一个构造函数,如果我们没有显式定义构造函数,那么它将会有一个默认无参的构造函数。在编译生成的字节码中,这些构造函数会被命名成 () 方法,参数列表与 Java 语言书写的构造函数的参数列表相同。

我们知道,Java 要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性。事实上,这一点是在构造函数中保证的:Java 强制要求 Object 对象( Object 是J ava 的顶层对象,没有超类)之外的所有对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数,如果我们既没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会为我们自动生成一个对超类构造函数的调用,比如:

1
2
3
4
public class Test3 {
}

另外有种情况, 当有重载构造函数时,我们经常会在一个构造函数中去调用另外一个构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* @author: fanyuzeng
* @date: 2018/3/10 12:21
*/
class Test3 {
int i;
public Test3() {
//这里不是省略了 super() 而是根本就没有
this(0);
}
public Test3(int i) {
//super() 这里省略了
this.i = i;
}
public static void main(String[] args) {
Test3 test3 = new Test3();
Utils.print("test3.i=" + test3.i);
}
}

对于这种情况, Java 只允许在 Test(int i) 中去调用超类的构造函数。

Java 做出这种限制,就是为了一个类在使用前,能够正确的被初始化。

小结

一个类的创建过程,实际上是一个递归的过程,总结如下图:

递归的思想在 Android 中的事件分发机制以及 View 的绘制等过程中也有体现。拿事件分发来说,不同的是,事件分发是从上到下分发,然后从下到上递归,如下:

引用自: 图解 Android 事件分发机制 (作者:Kelin)

实例变量初始化、代码块初始化、构造函数初始化以及多态的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* @author: fanyuzeng
* @date: 2018/3/10 13:03
*/
class Test4 {
static class Foo {
int i = 1;
Foo() {
System.out.println(i); // -----------(1)
int x = getValue();
System.out.println(x); // -----------(2)
}
{
i = 2;
}
protected int getValue() {
return i;
}
}
//子类
static class Bar extends Foo {
int j = 1;
Bar() {
j = 2;
}
{
j = 3;
}
@Override
protected int getValue() {
return j;
}
}
public static void main(String... args) {
Bar bar = new Bar();
System.out.println(bar.getValue()); // -----------(3)
}
}

根据上面的总结,Foo 和 Bar 的构造函数等价于:

1
2
3
4
5
6
7
8
9
//Foo类构造函数的等价变换:
Foo() {
i = 1;
i = 2;
System.out.println(i);
int x = getValue();
System.out.println(x);
}
1
2
3
4
5
6
7
8
//Bar类构造函数的等价变换
Bar() {
Foo();
j = 1;
j = 3;
j = 2
}

这样程序就好看多了,我们一眼就可以观察出程序的输出结果。

  • 在通过使用 Bar 类的构造方法 new 一个 Bar 类的实例时,首先会调用 Foo 类构造函数,因此(1)处输出是2,这从Foo类构造函数的等价变换中可以直接看出。

  • (2) 处输出是 0,为什么呢?因为在执行 Foo 的构造函数的过程中,由于 Bar 重载了 Foo 中的 getValue 方法,所以根据Java 的多态特性可以知道,其调用的 getValue 方法是被 Bar 重载的那个 getValue 方法。但由于这时 Bar 的构造函数还没有被执行,因此此时 j 的值还是默认值 0,因此 (2) 处输出是 0。

  • 最后,在执行 (3) 处的代码时,由于 bar 对象已经创建完成,所以此时再访问 j 的值时,就得到了其初始化后的值 2,这一点可以从 Bar 类构造函数的等价变换中直接看出。

共82.3k字
0%
.gt-container a{border-bottom: none;}