对象创建的边界在哪里?那些你可能不知道的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创建的对象不会调用构造器
相关推荐
苏打水com16 分钟前
数据库进阶实战:从性能优化到分布式架构的核心突破
数据库·后端
间彧1 小时前
Spring Cloud Gateway与Kong或Nginx等API网关相比有哪些优劣势?
后端
间彧1 小时前
如何基于Spring Cloud Gateway实现灰度发布的具体配置示例?
后端
间彧1 小时前
在实际项目中如何设计一个高可用的Spring Cloud Gateway集群?
后端
间彧1 小时前
如何为Spring Cloud Gateway配置具体的负载均衡策略?
后端
间彧2 小时前
Spring Cloud Gateway详解与应用实战
后端
EnCi Zheng3 小时前
SpringBoot 配置文件完全指南-从入门到精通
java·spring boot·后端
烙印6013 小时前
Spring容器的心脏:深度解析refresh()方法(上)
java·后端·spring
Lisonseekpan3 小时前
Guava Cache 高性能本地缓存库详解与使用案例
java·spring boot·后端·缓存·guava
4 小时前
JUC专题 - 并发编程带来的安全性挑战之同步锁
后端