对象创建的边界在哪里?那些你可能不知道的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创建的对象不会调用构造器
相关推荐
AI小智39 分钟前
MCP:昙花一现还是未来标准?LangChain 创始人激辩实录
后端
bobz96541 分钟前
strongswan IKEv1 proposal 使用
后端
Sans_1 小时前
初识Docker-Compose(包含示例)
后端·docker·容器
信阳农夫1 小时前
Django解析跨域问题
后端·python·django
小华同学ai1 小时前
331K star!福利来啦,搞定所有API开发需求,这个开源神器绝了!
前端·后端·github
hyena2 小时前
基于FastExcel/EasyExcel多线程导出百万级数据Excel文件并利用 ResponseBodyEmitter 与caffeine技术实现进度查询
后端
用户81367016107882 小时前
Spring MVC 请求处理流程
后端
世界哪有真情2 小时前
3月12日最新!Cursor无限续杯
前端·后端·cursor
Asthenia04122 小时前
深入剖析 Java 反射 Method.invoke 的底层原理:MethodAccessor->NativeMethodAccessorImpl
后端
TiHie2 小时前
Aviator规则引擎组件开发
后端