用关键字new
进行对象的创建,几乎是写代码时最常用的操作之一了,比如:
Cat cat1 = new Cat(); Cat cat2 = new Cat( "code", 18 );
通过new
的方式,我们可以调用类的无参或者有参构造方法来实例化出一个对象。
表面上看,简简单单new
一下对象就有了,但面试时如果仅仅答到这一层,大概率会扑街,因为比这个更重要的是new
对象时的原理和流程,因为JVM
这个牵线红娘在背后默默地帮我们做了很多工作。
说到new
一个对象的具体流程,用一张图可大致描述成如下所示:
new
一个对象时,比如Cat cat = new Cat()
,JVM
首先就回去检查Cat
这个符号引用所代表的类是否已经被加载过,如果没有就要执行对应类的加载过程;Cat cat = new Cat()
就会声明一个Cat
类型的引用cat
;JVM
就会在堆上为对象分配内存;0
”值初始化非常好理解,即为实例化对象的各个属性赋上默认初始化“0”值,比如int的初始化0值就是0,而一个对象的初始化0值就是null;private String name = "code";
就在这时候给初始化上;应该说,经过了这一系列步骤,一个新的可用对象方才得以诞生。
学过Java反射机制的都知道,只要能拿到类的Class
对象,就可以通过强大的反射机制来创造出实例对象了。
一般来说,拿到Class
对象有三种方式:
类名.class
对象名.getClass()
Class.forName(全限定类名)
有了Class
对象之后,接下来就可以调用其newInstance()
方法来创建一个对象,就像这样:
Cat cat3 = (Cat) Class.forName( "xxx.xxx.Cat" ).newInstance(); //全类名 Cat cat4 = Cat.class.newInstance();
当然,这种方式的局限性也有目共睹,因为使用的是类的无参构造方法来创建的对象。
所以比这个更进一步的方式是通过java.lang.relect.Constructor
这个类的newInstance()
方法来创建对象,因为它可以明确指定某个构造器来创建对象。
比如,在我们拿到了类的Class
对象后,就可以通过getDeclaredConstructors()
函数来获取到类的所有构造函数列表,这样我们就可以调用对应的构造函数来创建对象了,就像这样:
Constructor<?>[] constructors = Cat.class.getDeclaredConstructors(); Cat cat5 = (Cat) constructors[0].newInstance(); Cat cat6 = (Cat) constructors[1].newInstance( "code", 18 );
而且,如果我们想明确获取类的某个构造函数,也可以在getDeclaredConstructors()
函数里直接指定构造函数传参类型来精确控制,就像这样:
Constructor constructor = Cat.class.getDeclaredConstructor( String.class, Integer.class ); Cat cat7 = (Cat) constructor.newInstance( "code", 18);
对象克隆在我们日常写代码的时候基本上是刚性需求,基于一个对象克隆出另一个对象,这也是写Java代码时十分常见的操作。
关于对象拷贝这一知识点,详细梳理过一篇:《一个工作三年的同事,居然还搞不清深拷贝、浅拷贝…》,里面详细梳理了对象赋值、拷贝、深拷贝、浅拷贝等系列知识点,本文便不再赘述了。
Food对象重写clone方法:
public class Food implements Cloneable{ private String name; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
Cat对象重写clone方法并将Cat中的对象进行克隆:
public class Cat implements Cloneable { private String id; private String name; private Food food; @Override protected Object clone() throws CloneNotSupportedException { //这里只能实现浅拷贝 Cat cat =(Cat) super.clone(); //将food对象也进行克隆 Food food2 =(Food) food.clone(); cat.setFood(food2); return cat; }
如果对象嵌套的层级比较深这样就不好了,可以使用反序列化实现深拷贝
对象要进行序列化就必须实现Serializable接口,否则会抛出java.io.NotSerializableException异常,实际是因为在ObjectOutputStream中会对对象进行obj instanceof Serializable判断。
所以第二种实现方式如下:
//顶层类 public Cat deepCopy() { try { // 将对象本身序列化到字节流 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream( bos ); oos.writeObject( this ); // 再将字节流通过反序列化方式得到对象副本 ObjectInputStream ois = new ObjectInputStream( new ByteArrayInputStream( bos.toByteArray() ) ); return (Cat) ois.readObject(); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } return null; } //其他类一定要实现Serializable,否则抛出异常Food ...NotSerializableException
public static void main(String[] args) throws CloneNotSupportedException { Food food = new Food("fish"); Cat cat = new Cat("001", "code",food); Cat cloneCat = (Cat)cat.clone(); //deepCopy() System.out.println(cat == cloneCat); //false System.out.println(cat.getId()); //001 System.out.println(cloneCat.getId()); //001 System.out.println(cat.getFood()); //com.ultrapower.amr.webapi.util.Food@387c703b System.out.println(cloneCat.getFood()); //com.ultrapower.amr.webapi.util.Food@3f3afe78 }
关于对象「序列化和反序列化」这个知识点,重要且有用,但听很多朋友反映初学时有点糊。当我们作序列化和反序列化操作时,背后也会创建对象,关于「序列化和反序列化」这个知识点的详细理解+梳理,之前我也写过了,链接在此:序列化/反序列化,我忍你很久了,淦!。
Unsafe
类这个名字一听就有点悬了,的确,我们平时的业务代码里接触得好像并不多。
我们都知道写Java
代码,很少会去操作位于底层的一些资源,比如内存等这些。而位于sun.misc.Unsafe
包路径下的Unsafe
类提供了一种直接访问系统资源的途径和方法,可以进行一些底层的操作。比如借助Unsafe
我们就可以分配内存、创建对象、释放内存、定位对象某个字段的内存位置甚至并修改它等等。
可见这玩意误用时的破坏力是很大的,所以一般也都是受控使用的。业务代码里很少能看到它的身影,但是JDK
内部的一些诸如io
、nio
、juc
等包中的代码里还是有不少关于它的身影存在的。
Unsafe
类中有一个allocateInstance()
方法,通过其就可以创建一个对象。为此我们只需要获取到一个Unsafe
类的实例对象,我们自然就可以调用allocateInstance()
来创建对象了。
那如何才能获取到一个Unsafe
类的实例对象呢?
大致瞅一眼Unsafe
类的源码我们就会发现,它是一个单例类,其构造方法是私有的,所以直接构造是不太现实了:
public final class Unsafe { private static final Unsafe theUnsafe; // ... 省略 ... private static native void registerNatives(); private Unsafe() { } @CallerSensitive public static Unsafe getUnsafe() { Class var0 = Reflection.getCallerClass(); if (!VM.isSystemDomainLoader(var0.getClassLoader())) { throw new SecurityException("Unsafe"); } else { return theUnsafe; } } // ... 省略 ... }
而且获取单例对象的入口函数getUnsafe()
上也做了特殊标记,意思是只能从引导加载的类才可以调用该方法,这意味着该方法也是供JVM
内部使用的,外部代码直接使用会报类似这样的异常:
Exception in thread "main" java.lang.SecurityException: Unsafe
走投无路,我们只能再次重拾强大的反射机制来创建Unsafe
类的实例了:
Field f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); Unsafe unsafe = (Unsafe) f.get(null);
然后接下来我们就可以愉快地利用它来创建对象了:
Cat cat8 = (Cat) unsafe.allocateInstance( Cat.class );
当然除了上述这几种显式地对象创建场景之外,还有一些我们并没有进行手动对象创建的隐式场景,举几个常见例子。
我们都知道JVM
虚拟机在加载一个类的时候,也都会创建一个类对应的Class
实例对象,很明显这一过程是JVM
偷偷地背着我们干的。
典型的,比如定义一个String
类型的字面变量时,就可能会引起一个新的String
对象的创建,就像这样:
String name = "codesheep";
还常见的比如String
的+
号连接符也会隐式地导致新String
对象的创建等:
String str = str1 + str2;
这种例子也有很多,比如在执行类似如下代码时:
Integer codeSheepAge = 18;
其触发的自动装箱机制就会导致一个新的包装类型的对象在后台被隐式地创建出来。
比如像下面这样,当我们使用可变参数语法int... nums
来描述一个函数的入参时:
public double avg( int... nums ) { double sum = 0; int length = nums.length; for (int i = 0; i<length; ++i) { sum += nums[i]; } return sum/length; }
从表面上看,函数的调用处可以传入各种离散参数参与计算:
avg( 2, 2, 4 ); avg( 2, 2, 4, 4 ); avg( 2, 2, 4, 4, 5, 6 );
而背地里可能会隐式地产生一个对应的数组对象进行计算。
总而总之,很多场景下对象的隐式创建也是数见不鲜,我们最起码要做到心中大致有数。