对象创建的边界在哪里?那些你可能不知道的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创建的对象不会调用构造器
相关推荐
她的男孩4 小时前
多租户和数据权限怎么共存?扒完拦截器注册链路,我找到 4 个隐蔽的坑
java·后端·架构
用户8356290780514 小时前
使用 Python 设置 Excel 页眉和页脚
后端·python
步行cgn4 小时前
IoC 控制反转:从概念到 Spring 的实现
后端
爱勇宝4 小时前
没有 Fn 键关触控板?我做了一个双击即用的 Windows 小工具
前端·后端·程序员
沙湖遇雨5 小时前
8.ByteBuf 的数据模型、生命周期和内存分配
后端
by组态5 小时前
Ricon组态系统API参考手册
前端·后端·物联网
再吃一根胡萝卜5 小时前
Agent 如何选择与生成 SQL —— Text2SQL 全链路详解
后端
苍何5 小时前
把操作流程直接录成 Skill,豆包太夯了!
后端