文章目录
为了方便后边的代码展示,我们先提前定义一个演示类:
@Data
public class User {
private String userName;
private short age;
public User() {
System.out.println("------ 调用无参构造方法 ------");
}
public User(String userName, short age) {
this.userName = userName;
this.age = age;
System.out.println(String.format("------ 调用有参构造方法:userName[%s], age[%d] ------", userName, age));
}
}
1、使用 new 关键字创建对象
这是最常见也是最简单的创建对象的方式。通过这种方式,我们可以调用定义的构造函数(无参的和带参数的)来创建对象。
如:
public static void main(String[] args) {
User user1 = new User();
User user2 = new User("Lucy", (short) 18);
System.out.println(user1);
System.out.println(user2);
}
输出:
------ 调用无参构造方法 ------
------ 调用有参构造方法:userName[Lucy], age[18] ------
User(userName=null, age=0)
User(userName=Lucy, age=18)
2、使用 Constructor 类的 newInstance 方法
通过 Class 类对象的 getConstructor() 或 getDeclaredConstructor() 方法获得构造器(Constructor)对象,并调用其 newInstance() 方法创建对象,适用于无参和有参构造方法。
如:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException,
InvocationTargetException, InstantiationException {
// 获取 User 的无参构造函数,并创建对象
User user1 = User.class.getDeclaredConstructor().newInstance();
// 获取 User 的第一个参数是String, 第二个参数是short的构造函数,并创建对象
User user2 = User.class.getDeclaredConstructor(String.class, short.class).newInstance("Lili", (short)17);
System.out.println(user1);
System.out.println(user2);
}
输出:
------ 调用无参构造方法 ------
------ 调用有参构造方法:userName[Lili], age[17] ------
User(userName=null, age=0)
User(userName=Lili, age=17)
这是通过反射创建对象的一种方式,这里 getConstructor() 和 getDeclaredConstructor() 区别是:
- getConstructor : 构造函数的访问权限必须是 public
- getDeclaredConstructor : 构造函数可以是任意访问权限
通过这种方式可以调用类私有的构造方法,但需要禁用构造器对象的安全检查。
如:
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException,
InvocationTargetException, InstantiationException {
Constructor<User> constructor = User.class.getDeclaredConstructor();
// 禁用 constructor 构造器对象的安全检查
constructor.setAccessible(true);
User user = constructor.newInstance();
System.out.println(user);
}
禁用安全检查后,就可以通过私有的构造方法创建新对象了。
3、使用 Class 类的 newInstance 方法
这种方式跟上边第 2 种方式差不多,都是通过反射来创建对象。内部其实也是先获取到 Constructor 构造器对象,然后再使用 Constructor 类的 newInstance 方法创建对象。不过通过这种方式创建对象,调用的是无参构造方法,并且此无参构造方法不能是 private (Class 类的 newInstance 方法中做了访问类型校验)。
如:
public static void main(String[] args) throws IllegalAccessException, InstantiationException {
User user = User.class.newInstance();
System.out.println(user);
}
输出:
------ 调用无参构造方法 ------
User(userName=null, age=0)
4、使用 clone 方法
想要通过 clone 方式创建对象,则需要对 User 类做一些改动:
- User 类实现 Cloneable 接口;
- User 类中,重写 Object 类中的 clone() 方法。
这里有个有意思的地方,查看 Cloneable 接口源码,我们可以看到,这个接口中一个方法也没有。重写的 clone() 方法是 Object 这个超级父类中的。
那这里的 Cloneable 接口有什么用呢?
其实,这里的 Cloneable 接口起到的是一个标记的作用,在JVM中,具有这个标记的对象才有可能被拷贝。那怎么才能从“有可能被拷贝” 转换为 “可以被拷贝” 呢? 那就是重写 Object 类的 clone() 方法。
改动后的 User 类如下:
@Data
public class User implements Cloneable{
private String userName;
private short age;
protected User() {
System.out.println("------ 调用无参构造方法 ------");
}
public User(String userName, short age) {
this.userName = userName;
this.age = age;
System.out.println(String.format("------ 调用有参构造方法:userName[%s], age[%d] ------", userName, age));
}
@Override
protected User clone() throws CloneNotSupportedException {
System.out.println("------ 调用 clone 方法 ------");
return (User)super.clone();
}
}
接下来试下通过 clone 创建对象:
public static void main(String[] args) throws CloneNotSupportedException {
User user = new User("Lilei", (short) 20);
// 通过 clone 方式创建对象
User cloneUser = user.clone();
System.out.println(user);
System.out.println(cloneUser);
// 看下对象是否是同一个对象
System.out.println(user == cloneUser);
}
输出:
------ 调用有参构造方法:userName[Lilei], age[20] ------
------ 调用 clone 方法 ------
User(userName=Lilei, age=20)
User(userName=Lilei, age=20)
false
通过 clone 方式创建对象,并不会调用任何构造函数,而是直接在内存中对二进制流的拷贝,要比直接 new 一个对象性能好很多。
注意:在使用 clone 方式创建对象时,需要注意深拷贝和浅拷贝的不同,不了解的可以百度下,这里就不进行扩展了。
5、使用 Serializable 反序列化方式
想要通过 Serializable 反序列化方式创建对象,则类需要实现 Serializable 接口。这里 Serializable 接口也是一个标记的作用,当我们让实体类实现 Serializable 接口时,其实是在告诉JVM此类可被默认的序列化机制序列化。
改动后的 User 类如下:
@Data
public class User implements Serializable {
private static final long serialVersionUID = 7330451128451152699L;
private String userName;
private short age;
protected User() {
System.out.println("------ 调用无参构造方法 ------");
}
public User(String userName, short age) {
this.userName = userName;
this.age = age;
System.out.println(String.format("------ 调用有参构造方法:userName[%s], age[%d] ------", userName, age));
}
}
接下来试下通过反序列化方式创建对象:
public static void main(String[] args) throws IOException, ClassNotFoundException {
User user = new User("Hanmeimei", (short) 18);
// 通过 ObjectOutputStream 对象输出流,将 user 对象序列化写入到 user.txt 文件中
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\user.txt"));
out.writeObject(user);
out.close();
// 通过 ObjectInputStream 对象输入流,将 user.txt 文件中内容反序列化,创建一个 User 对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\user.txt"));
User user_1 = (User)in.readObject();
// 再次通过 ObjectInputStream 对象输入流,将 user.txt 文件中内容反序列化,创建一个 User 对象
in = new ObjectInputStream(new FileInputStream("D:\\user.txt"));
User user_2 = (User)in.readObject();
System.out.println(user);
System.out.println(user_1);
System.out.println(user_2);
// 判断两次反序列化创建的对象是否是同一个对象
System.out.println(user_1 == user_2);
}
输出:
------ 调用有参构造方法:userName[Hanmeimei], age[18] ------
User(userName=Hanmeimei, age=18)
User(userName=Hanmeimei, age=18)
User(userName=Hanmeimei, age=18)
false
通过 Serializable 反序列化方式创建对象,也不会调用任何构造函数。如果想控制哪个字段属性不被序列化和反序列化,可以通过 transient 关键字,将属性指定为瞬时态。
上边是使用的JVM默认的序列化和反序列化逻辑,如果想要使用自定义的序列化和反序列化,需要添加两个方法 private void writeObject(ObjectOutputStream out)
和 private void readObject(ObjectInputStream in)
改动后的 User 类如下:
@Data
public class User implements Serializable {
private static final long serialVersionUID = 7330451128451152699L;
private String userName;
private short age;
public User() {
System.out.println("------ 调用无参构造方法 ------");
}
public User(String userName, short age) {
this.userName = userName;
this.age = age;
System.out.println(String.format("------ 调用有参构造方法:userName[%s], age[%d] ------", userName, age));
}
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeShort(age); // 序列化时,只写入 age 的值(哪些序列化哪些不序列化,都由我们自己控制)
System.out.println("------ 调用 writeObject 方法 ------");
}
private void readObject(ObjectInputStream in) throws IOException,
ClassNotFoundException {
age = in.readShort(); // 反序列化时,只读取 age 的值,这个顺序需要跟写入顺序一致
System.out.println("------ 调用 readObject 方法 ------");
}
}
再来试下反序列化方式创建对象:
public static void main(String[] args) throws IOException, ClassNotFoundException {
User user = new User("Hanmeimei", (short) 18);
// 通过 ObjectOutputStream 对象输出流,将 user 对象序列化写入到 user.txt 文件中
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\user.txt"));
out.writeObject(user);
out.close();
// 通过 ObjectInputStream 对象输入流,将 user.txt 文件中内容反序列化,创建一个 User 对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\user.txt"));
User user_1 = (User)in.readObject();
System.out.println(user);
System.out.println(user_1);
}
输出:
------ 调用有参构造方法:userName[Hanmeimei], age[18] ------
------ 调用 writeObject 方法 ------
------ 调用 readObject 方法 ------
User(userName=Hanmeimei, age=18)
User(userName=null, age=18)
这里为什么加上 writeObject
和 readObject
方法后,就可以实现自定义的序列化和反序列化呢?这是因为 ObjectOutputStream 和 ObjectInputStream 使用反射来寻找是否声明了这两个方法。如果声明了这俩方法,并且访问类型是私有的,则使用这两个方法来进行序列化和反序列化。
注意1:使用自定义序列化和反序列化时,可以序列化和反序列化瞬态变量,因此 transient 没有效果。
注意2:serialVersionUID 用来表明实现序列化类的不同版本间的兼容性。如果类内容没有修改,或者修改后的类兼容之前的类,请不要修改此值,否则之前序列化的类在反序列化时会报错。
6、使用 Externalizable 反序列化方式
Externalizable 是 Serializable 接口的子接口,它也允许我们控制整个序列化过程。通过这种方式反序列化创建对象时,会先调用默认的无参构造函数创建对象(访问类型必须是 public ),然后再将反序列化读取到的值填充到对象中。
使用 Externalizable 方式,需要实现 writeExternal(ObjectOutput out) 和 readExternal(ObjectInput in) 方法的逻辑。
我们来看下 User 改动后:
@Data
public class User implements Externalizable {
private static final long serialVersionUID = 7330451128451152697L;
private String userName;
private short age;
public User() {
System.out.println("------ 调用无参构造方法 ------");
}
public User(String userName, short age) {
this.userName = userName;
this.age = age;
System.out.println(String.format("------ 调用有参构造方法:userName[%s], age[%d] ------", userName, age));
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeShort(age); // 序列化时,只写入 age 的值(哪些序列化哪些不序列化,都由我们自己控制)
System.out.println("------ 调用 writeExternal 方法 ------");
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
age = in.readShort(); // 反序列化时,只读取 age 的值,这个顺序需要跟写入顺序一致
System.out.println("------ 调用 readExternal 方法 ------");
}
}
接下来试下通过反序列化方式创建对象:
public static void main(String[] args) throws IOException, ClassNotFoundException {
User user = new User("Hanmeimei", (short) 18);
// 通过 ObjectOutputStream 对象输出流,将 user 对象序列化写入到 user.txt 文件中
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\user.txt"));
out.writeObject(user);
out.close();
// 通过 ObjectInputStream 对象输入流,将 user.txt 文件中内容反序列化,创建一个 User 对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\user.txt"));
User user_1 = (User)in.readObject();
System.out.println(user);
System.out.println(user_1);
}
输出:
------ 调用有参构造方法:userName[Hanmeimei], age[18] ------
------ 调用 writeExternal 方法 ------
------ 调用无参构造方法 ------
------ 调用 readExternal 方法 ------
User(userName=Hanmeimei, age=18)
User(userName=null, age=18)
注意1:使用 Externalizable,可以序列化/反序列化瞬态变量,因此 transient 没有效果。
注意2:serialVersionUID 用来表明实现序列化类的不同版本间的兼容性。如果类内容没有修改,或者修改后的类兼容之前的类,请不要修改此值,否则之前序列化的类在反序列化时会报错。
6种创建对象方式的总结
最后,我们来看下6种创建对象方式的区别:
方式 | 是否用到构造函数 | 特点 |
---|---|---|
使用 new 关键字 | 是 | 简单 |
使用 Constructor 类的 newInstance | 是 | 可以使用任何访问类型的构造函数 |
使用 Class 类的 newInstance | 是 | 只能使用非私有的无参构造函数 |
使用 clone 方法 | 否 | 直接在内存中对二进制流的拷贝,性能好 |
使用 Serializable 反序列化 | 否 | |
使用 Externalizable 反序列化 | 是 | 相比 Serializable ,可以实现构造方法的初始化过程(必须是 public 的无参构造方法) |