谈谈Java中创建对象的10种方式

Java
248
0
0
2023-11-09
标签   Java基础

前言

在创建对象的时候使用最多的就是new关键字进行创建。除了使用new关键字外 java 中还存在其他多种创建对象的方法,如下。

  1. new 关键字
  2. 反射一:Class#newInstance
  3. 反射二: Constructor #newInstance
  4. 克隆
  5. 反序列化
  6. 字符串 字面量
  7. 字符串拼接
  8. 自动装箱
  9. lambda表达式
  10. 方法引用

new关键字

new关键字是直接调用 构造器 ,是java中最常见的创建方式:

  new A(); 

反射一:java.lang.Class#newInstance

java.lang .Class#newInstance创建对象时就像调用类的无参构造器一样,它会把构造器中抛出的异常也给抛出来。

  public class A {
     public A() {
         System.out.println("A constructor");
         throw new Runtime Exception ("not support");
    }
 } 
 public class  Client  {
    public  static   void  main(String[] args) throws InstantiationException, IllegalAccessException {
        A.class.newInstance();
    }
} 

执行结果

java.lang.Class#newInstance的原理也是通过获取该类下的无参构造器来实现的,如源码图中的412行用于获取无参构造器,442行用于调用无参构造器。

java.lang.Class#newInstance源码1

java.lang.Class#newInstance源码2

该方式调用构造器同样也会受到构造器的访问权限的限制,如源码中的436行会对访问权限进行检查,如果无权访问会抛出java.lang.IllegalAccessException。

反射二:java.lang.reflect.Constructor#newInstance

该方式会调用构造器进行创建,构造器中抛出的异常会包装成java.lang.reflect.InvocationTargetException,同样也会受到构造器的访问修饰符的限制。

调用方式一:获取public权限的无参构造器进行调用:

A.class.getConstructor().newInstance(); 

调用方式二:获取第一个public权限的构造器进行调用,注意返回的构造器数组中的元素是与构造器在类中定义的顺序一致的,先定义的构造器在数组中排名靠前,如下图我们A类中定义的无参构造器在第一位,则可以通过数组中的 索引 0进行访问执行。

  A.class.getConstructors()[].newInstance(); 

调用方式三:通过getDeclaredConstructor获取具有所有访问权限的无参构造器进行创建,如果构造器不可访问可以通过setAccessible(true)设置访问权限:

  Constructor<A> declaredConstructor = A.class.getDeclaredConstructor();
 declaredConstructor.setAccessible(true);
 declaredConstructor.newInstance(); 

调用方式四:通过getDeclaredConstructors获取具有所有访问权限构造器数组进行创建,构造器在数组中的顺序同样是和构造器定义的顺序一致的。

  Constructor<?> declaredConstructor = A.class.getDeclaredConstructors()[];
 declaredConstructor.setAccessible(true);
 declaredConstructor.newInstance(); 

克隆

定义的A类如下,如果要使用clone方法创建对象,那么类必须重写clone方法并且实现Cloneable接口。Cloneable接口只是一个标记接口,本身并没有任何实现。

我们在A类的构造器中打印一下信息,重写后的clone方法只是调用了一下父类的clone方法,并没有其他操作。

  public class A implements Cloneable {
     public A() {
         System.out.println("A Constructor");
    }
 
     @Override
     protected Object clone() throws CloneNotSupportedException {
         return super.clone();
    }
 } 

Client类用于创建A类的对象origin,并使用origin对象进行克隆:

 public class Client {
    public static void main(String[] args) throws CloneNotSupportedException {
        A origin = new A();//此时会调用构造器
        A clone = (A) origin.clone();//不会调用构造器
        System.out.println(origin == clone);
        System.out.println(origin.equals(clone));
    }
} 

执行Client类,查看执行结果:

执行结果

从结果中可以看出A类构造器中的打印语句执行了一次,说明进行克隆时并不是通过构造器进行的新对象的创建。

反序列化

ObjectOutputStream类用于把原始类型和对象写入到一个OutputStream。 OutputStream 的具体实现负责存储写入的对象,这个实现可以是一个字节数组输出流、文件输出流和网络 socket 输出流。如果是一个网络socket输出流这个对象也可以在另一台机器上进行构建。

只有实现了java.io. Serializable接口 的类的对象才能通过序列化写入到输出流中。写入的对象可以使用ObjectInputStream类进行读取。

我们以A类为操作对象来进行操作:

  public class A implements  Serializable  {
     public A() {
         System.out.println("A Constructor");
    }
 } 

在Client类中进行如下操作,本例中我们用一个字节数组输出流进行对象的存储。对A类对象进行写入的方法为writeObject方法。对写入的对象进行读取时使用的方法是readObject方法。具体操作如下图:

 public class Client {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        //创建一个字节输出流,将对象真正输出的地方
         byte ArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
        //创建一个对象输出流,用来写入对象。
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(arrayOutputStream);
        //写入对象到字节输出流中
        objectOutputStream.writeObject(new A());

        //创建一个字节输入流,将已经写入对象的字节输出流转换成字节数组作为参数传入
        ByteArray InputStream  byteArrayInputStream = new ByteArrayInputStream(arrayOutputStream.toByteArray());
        //创建一个对象输入流,将已经读取对象的字节输入流作为参数传入
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        //读取对象并转换为A类的对象
        A o = (A) objectInputStream.readObject();//不会调用构造器
        // 打印
        System.out.println(o);
    }
} 

当我们执行完Client类后,可以看到打印的信息如下,其中第一个A Constructor是我们代码中将写入对象时创建的A的对象。可以看出反序列化时,A类的构造器并没有调用。

执行结果

字符串字面量

String类型可以通过字面量直接创建一个String对象:

  String string = "dog"; 

字符串拼接

可以通过字符串拼接创建一个String对象,如下的string2:

  String string = "dog";
 String string = string + 0; 

自动装箱

int类型的变量直接赋值给一个 Integer 类的变量时,会把原始类型自动装箱生成一个对应的新对象:

  int i =;
 Integer j = i; 

或将原始类型作为参数传入时也会生成一个新的对象:

  public static void main(String[] args) {
     function();
}
 public static void function(Integer j) {
     System.out.println(j);
} 

lambda表达式

如图中的Runnable runnable = ()->{};其中runnable属于新建的对象,该对象所属的类型为Client$$Lambda$1/225493257

lambda表达式

方法引用

方法引用和lambda类似,如图中变量supplier,它也是一个对象引用了Client类的构造器,打印其类型信息为Client$$Lambda$1/1348949648

方法引用

总结

  1. new 关键字会调用构造器
  2. java.lang.Class#newInstance会调用无参构造器,会抛出构造器中的异常。
  3. java.lang.reflect.Constructor#newInstance会调用构造器,可以传入构造器参数来调用不同的构造器进行新建对象。会把构造器中的异常包装成ava.lang.reflect.InvocationTargetException并抛出
  4. clone方法不会调用构造器,要实现java.lang.Cloneable,并且重写clone方法。属于浅克隆,无法复制对象中除了基本类型和String字面量类型的其他引用对象
  5. 反序列化不会调用构造器,但是要求实现java.io.Serializable,效率较低,属于深克隆。