对象创建的边界在哪里?那些你可能不知道的Java黑魔法

平时在写java代码的时候,创建对象都是使用的new关键字,框架中呢,常使用反射创建对象。后来在看八股的时候看到一个问题,问java中创建对象的方式,当时没专门了解过,思来想去也只想到了这两个方法,还是平时的积累太薄弱了,于是我就想来专门的记录一篇文章,用来记录一下这个问题。


我们在介绍之前,先准备一个自己的对象

我这里简单创建一个User类,后续的代码都会在这个User类上面进行操作

java 复制代码
public class User {
    private String name;
    private int age;
    
    
    public User() {
        // 在构造函数中打印一句话,后面可以用来查看创建对象时是否使用了构造函数
        System.out.println("通过构造函数创建 User 对象");
    }

    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;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
    }
}

1. new 关键字

第一个方法毫无疑问就是我们最常用的new关键字了,这个大家都知道,就不做过多的描述了。

直接给出main函数的代码。

java 复制代码
public class Main {
    public static void main(String[] args) {
        User user = new User();
    }
}

2. 反射

我们通过 UserClass 类对象获取这个类的构造器对象,然后调用 Constructor 对象的newInstance()方法来创建对象。

java 复制代码
public class Main {
    public static void main(String[] args) {
        try {
            Constructor<User> constructor = User.class.getDeclaredConstructor();
            User user = constructor.newInstance();
            user.setName("张三");
            user.setAge(18);
            System.out.println(user);
        } catch (Exception e) {
            System.out.println("出异常了!");
        }
    }
}

从运行截图可以看出来,对象创建成功,并且调用了 User类的构造器。

该方法可以通过暴力反射调用私有的构造器 constructor.setAccessible(true);

3. 克隆

我这里重点是创建对象,就不介绍深克隆和浅克隆了。

克隆方式创建对象需要让User实现Cloneable接口并重写clone()方法,此方法是 protected 的,不重写无法调用。
User类修改部分代码:

java 复制代码
public class User implements Cloneable{
    private String name;
    private int age;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

main函数

java 复制代码
public class Main {
    public static void main(String[] args) {
        try {
            User user = new User();
            user.setAge(18);
            user.setName("张三");
            User clone = (User) user.clone();
            System.out.println(clone);
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

运行结果:

注意:

  • 出来的对象和原来的对象的值是一样的,用类型只是复制了引用类型的值(浅克隆)
  • 克隆的方式创建对象不会调用该类型的构造方法,运行结果图中的构造器调用是创建原始对象是调用的,克隆对象没有调用构造方法

4. 反序列化

使用反序列化的方式创建对象,需要实现Serializable接口。

我这里是直接序列化成的字节数组,持久化到磁盘文件是一样的

java 复制代码
public class Main {
    public static void main(String[] args) {
        User user = new User();
        user.setAge(18);
        user.setName("张三");
        try {
            // 序列化
            byte[] bytes = serialize(user);
            // 反序列化为对象
            User user1 = deserialize(bytes);
            System.out.println(user1);
            System.out.println(user1 == user);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }
    // 把对象序列化成 byte[] 数组
    public static byte[] serialize(User user) throws IOException {
        ByteArrayOutputStream bs = new ByteArrayOutputStream();
        ObjectOutputStream os = new ObjectOutputStream(bs);
        // 使用对象流把对象写入
        os.writeObject(user);
        byte[] bytes = bs.toByteArray();
        return bytes;
    }
    // 反序列化为对象
    public static User deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
        ByteArrayInputStream bs = new ByteArrayInputStream(bytes);
        ObjectInputStream os = new ObjectInputStream(bs);
        return (User) os.readObject();
    } 
    
}

运行截图:

  • 反序列化不会调用User的构造方法,但是会调用父类的构造方法,这里就不演示了
  • 反序列化的对象和原来的对像不是用一个对象

5. MethodHandle

这种方法类似于反射,但是它是另一套 API
indConstructor(User.class, MethodType.methodType(void.class))这个方法是找到对应的构造器,第一个参数的类的类型,第二个参数是MethodType,分别填入返回值类型,和对应参数的类型。

java 复制代码
public class Main {
    public static void main(String[] args) {
        try {
            MethodHandle constructor = MethodHandles.lookup().findConstructor(User.class, MethodType.methodType(void.class));
            // 调用构造方法
            User user = (User) constructor.invoke();
            user.setName("张三");
            user.setAge(21);
            System.out.println(user);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

运行截图:

6. Unsafe

可以通过Unsafe来创建对象,因为Unsafe有些特别,我这里使用反射来获取Unsafe的对象。

java 复制代码
public class Main {
    public static void main(String[] args) {
        try {
            Class klass = Unsafe.class;
            Field field = klass.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            Unsafe unsafe = (Unsafe) field.get(null);

            User user = (User) unsafe.allocateInstance(User.class);
            user.setName("张三");
            user.setAge(16);
            System.out.println(user);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

运行截图:

  • 通过Unsafe创建的对象不会调用构造器
相关推荐
caihuayuan56 小时前
升级element-ui步骤
java·大数据·spring boot·后端·课程设计
Kookoos8 小时前
ABP vNext + EF Core 实战性能调优指南
数据库·后端·c#·.net·.netcore
揣晓丹9 小时前
JAVA实战开源项目:健身房管理系统 (Vue+SpringBoot) 附源码
java·vue.js·spring boot·后端·开源
豌豆花下猫10 小时前
Python 3.14 新特性盘点,更新了些什么?
后端·python·ai
caihuayuan511 小时前
Vue生命周期&脚手架工程&Element-UI
java·大数据·spring boot·后端·课程设计
明月与玄武14 小时前
Spring Boot中的拦截器!
java·spring boot·后端
菲兹园长14 小时前
SpringBoot统一功能处理
java·spring boot·后端
muxue17814 小时前
go语言封装、继承与多态:
开发语言·后端·golang
开心码农1号14 小时前
Go语言中 源文件开头的 // +build 注释的用法
开发语言·后端·golang
北极象14 小时前
Go主要里程碑版本及其新增特性
开发语言·后端·golang