引子

首先,一个常问的问题,Java中常用的实例化一个对象的方法有哪些?答案可以简单归类为以下几种方式

  1. 用new 语句创建对象,这是最常用的创建对象方法。这是我们最常用的最基本的方法
  2. 运用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法。在我们需要将java对象存储到磁盘保存或者通过网络传输的时候会用到这种方法,但是缺点是java自带的这种序列化方式后字节码较大、效率不高。目前来说需要对Java对象序列化及反序列化的话有其他更好的方案
  3. 运用反射手段,调用Java.lang.Class或者java.lang.reflect.Constructor类的newInstance()实例方法。这是我们在需要动态创建对象,不确定类名的时候最常用的方法
  4. 以及调用对象的clone()方法。这种方式需要现有的Java类实现Cloneable接口,再Override Object类的clone()方法,且还存有浅拷贝、深拷贝的问题,Object的clone()方法默认是浅拷贝

关于Objenesis

除了上面这些,我们还有另外一个选项,使用Objenesis,Objenesis是一个小的Java库,它有一个用途:实例化一个特定类的新对象。

我们举一个栗子,比如如下这个类

public class TestObj {

    private final long userId;

    private String name;

    private int age;

    public TestObj(long userId){
        //todo check userId unique, or else throw Exception
        this.userId = userId;
    }

    public long getUserId() {
        return userId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

}

我们只创建了一个需要传入userId的构造函数,并在构造函数中需要做一些校验的工作保证当前构造的userId是唯一的,那么在遇到这种情况的时候,如果我们想要newInstance()实例话就不可能了。执行一句

TestObj.class.newInstance();

会报出如下错误

而如果使用Objenesis,我们可以实现这一点,通过绕过对象实例化的构造函数来克服这些限制。简单代码如下

TestObj testObj = new TestObj(123);
testObj.setAge(12);
testObj.setName("CheungQ");
Objenesis objenesis = new ObjenesisStd();
TestObj testObj2 = objenesis.newInstance(TestObj.class);
BeanUtils.copyProperties(testObj,testObj2);
Field[] fields = TestObj.class.getDeclaredFields();
for (Field field : fields) {
    field.setAccessible(true);
    field.set(testObj2,field.get(testObj));
}

最终可以得到两个userId均为123的对象

如果你还是觉得有所疑惑,我们不妨再修改下代码,这样可以看得更清楚些。新增一个StaticConstant类,他包含了一个变量HashSet HASH_SET

public class StaticConstant {
    public static final HashSet<Long> HASH_SET = new HashSet<>();

    private StaticConstant() {
    }
}

并把原来TestObj类的构造方法改造成如下,这种做法旨在保证当前内存中每一个TestObj对象的Obj都是唯一的,在某些类型的系统或者应用实践中会需要有这样的保证

    public TestObj(long userId) throws Exception {
        //todo check userId unique, or else throw Exception
        if (HASH_SET.contains(userId)){
            throw new Exception("userId should be unique");
        }
        HASH_SET.add(userId);
        this.userId = userId;
    }

此时如果执行

TestObj testObjExt = new TestObj(123);
TestObj testObj = new TestObj(123);

则会得到这样的异常信息

所以,通过以上的演示我们知道,Java已经支持使用class.newinstance()的类动态实例化,但是必须要有一个合适的构造函数。而很多场景下类不能够用这种方式去实例化,例如:

  • 构造函数需要参数(Constructors that require arguments)
  • 有副作用的构造函数(Constructors that have side effects)
  • 会抛出异常的构造函数(Constructors that throw exceptions)

因此,常见的是在类库中看到类必须要有一个默认的构造函数的限制。

需要在不调用构造函数的情况下实例化对象是一项相当特殊的任务,但是在某些情况下这是有用的:

  • 序列化,远程调用和持久化-对象需要被实例化并恢复到特定的状态,而不需要调用代码
  • 代理、 AOP 库和 mock 对象-类可以被子类继承而子类不用担心父类的构造器
  • 容器框架-对象可以以非标准的方式动态地实例化

简单使用

上面的示例中我们已经简单的使用过了Objenesis,这里我们再稍作展开再细说一下。最简单的使用Objenesis的方法是使用ObjenesisStd(标准)和ObjenesisSerializer(Serializable兼容)。默认情况下,自动确定最佳策略,所以你不必去考虑。

Objenesis objenesis = new ObjenesisStd(); // or ObjenesisSerializer

一旦有了Objenesis实现,就可以为特定类型类对象创建ObjectInstantiator。

ObjectInstantiator instantiator = objenesis.getInstantiatorOf(YourJavaObject.class);

最后,您可以使用它来实例化这种类型的新实例。

YourJavaObject obj = (YourJavaObject)instantiator.newInstance();

为了提高性能,最好尽可能多地重用ObjectInstantiator对象。例如,如果您正在实例化一个特定类的多个实例,那么从相同的ObjectInstantiator中执行它。InstantiatorAtorstrategy和ObjectInstantiator是线程安全的,他们都可以在多个线程之间共享和并发调用。http://objenesis.org/tutorial.html