对象创建的边界在哪里?那些你可能不知道的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创建的对象不会调用构造器
相关推荐
章豪Mrrey nical5 小时前
前后端分离工作详解Detailed Explanation of Frontend-Backend Separation Work
后端·前端框架·状态模式
派大鑫wink6 小时前
【JAVA学习日志】SpringBoot 参数配置:从基础到实战,解锁灵活配置新姿势
java·spring boot·后端
程序员爱钓鱼7 小时前
Node.js 编程实战:文件读写操作
前端·后端·node.js
xUxIAOrUIII7 小时前
【Spring Boot】控制器Controller方法
java·spring boot·后端
Dolphin_Home7 小时前
从理论到实战:图结构在仓库关联业务中的落地(小白→中级,附完整代码)
java·spring boot·后端·spring cloud·database·广度优先·图搜索算法
zfj3217 小时前
go为什么设计成源码依赖,而不是二进制依赖
开发语言·后端·golang
weixin_462446237 小时前
使用 Go 实现 SSE 流式推送 + 打字机效果(模拟 Coze Chat)
开发语言·后端·golang
JIngJaneIL7 小时前
基于springboot + vue古城景区管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
小信啊啊8 小时前
Go语言切片slice
开发语言·后端·golang
Victor35610 小时前
Netty(20)如何实现基于Netty的WebSocket服务器?
后端