Javaer搞对象的多种方式

前言 Preface

上次在soul上面看到一个问题,是关于创建对象(搞对象,哈哈!)的,我当时现出来了 new、反射、克隆、序列化这四种方式,但是当时激发了兴趣,所以就来总结一下,我目前掌握的几种方式吧。

提供一个用来测试的类:

package dragon.obj;

import java.io.Serializable;

public class Girl implements Cloneable, Serializable {
	/**
	 * 默认的序列化id
	 */
	private static final long serialVersionUID = 1L;
	private String name;
	private String length;
	private String weight;
	private String description;
	
	public Girl(String name, String length, String weight, String description) {
		super();
		this.name = name;
		this.length = length;
		this.weight = weight;
		this.description = description;
	}
	
	//省略 getter 和 setter方法,这个可以使用IDE自动生成,很方便的。
	
	@Override
	public String toString() {
		return "Girl [name=" + name + ", length=" + length + ", weight=" + weight + ", description=" + description
				+ "]";
	}
	
	@Override
	public Object clone() throws CloneNotSupportedException {
		return super.clone();
	}
}

new 搞对象

package dragon.obj;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

public class CreateObj {
	public static void main(String[] args) throws Exception {
		rawNew("伊人", "165", "110", "真可爱");
	}
	
	static void rawNew(String name, String length, String weight, String description) {
		Girl girl = new Girl(name, length, weight, description);
		System.out.println(girl);
	}
}

测试结果:
在这里插入图片描述

说明:基本上都会的一种方式,最开始学习的,也是最简单的创建对象的方式。

反射 搞对象

//包名及导包同上,此处不再提供。
public class CreateObj {
	public static void main(String[] args) throws Exception {
		reflect("伊人", "165", "110", "真可爱");
	}
	
	static void reflect(String name, String length, String weight, String description) {
		try {
			Class<?> clazz = Class.forName("dragon.obj.Girl");
			Constructor<?> con = clazz.getConstructor(String.class, String.class, String.class, String.class);
			Girl girl = (Girl) con.newInstance(name, length, weight, description);
			System.out.println(girl);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

测试结果:
在这里插入图片描述

说明:使用反射(reflect)的方式创建对象,很有趣的一种方式,关于如何使用可以自己对学习一下,掌握基本的使用是很简单的,我大概也就是这个层次了,哈哈!
获取Class对象的方式有三种:
1.对象.getClass();
2.类名.class;
3.Class.forName();

浅克隆 搞对象

注:浅克隆和深克隆都是设计模式——原型模式(protype pattern)。浅克隆使用的是native方法,速度上比较快的,但是它不能克隆引用类型变量。实现浅克隆需要相应的类实现Cloneable接口,并重写 clone方法。

//包名及导包同上,此处不再提供。
public class CreateObj {
	public static void main(String[] args) throws Exception {
		simpleClone("伊人", "165", "110", "真可爱");
	}
	
	/**
	 * 浅克隆不需要实现序列化,深克隆才需要。
	 * */
	static void simpleClone(String name, String length, String weight, String description) throws CloneNotSupportedException {
		Girl girl = new Girl(name, length, weight, description);
		Girl newGirl = (Girl) girl.clone();
		System.out.println(newGirl);
	};
}

运行截图:
在这里插入图片描述
说明:
学习设计模式的时候,用过浅克隆,但是之后就没有怎么使用过了,目前缺乏使用经验,只是知道这种实现方法,它也是创建对象的方式之一。

深克隆 搞对象

深克隆是为了克服浅克隆无法克隆引用类型变量的一种克隆对象的方式,实现深克隆需要借助Java的序列化机制,首先被克隆类要实现 Serialization 空接口,该接口只是一个标记。序列化可以实现对象的持久化存储,因此它可以用来使用深克隆。在程序中使用 ObjectOutputStream和ObjectInputStream两个流,将对象写入到存储介质中,或者把对象从存储介质中读取出来。

//包名及导包同上,此处不再提供。
public class CreateObj {
	public static void main(String[] args) throws Exception {
		deepClone("伊人", "165", "110", "真可爱");
	}
	
	static void deepClone(String name, String length, String weight, String description) {
		Girl girl = new Girl(name, length, weight, description);
		
		ByteArrayOutputStream out = new ByteArrayOutputStream();  //用于存放对象的输出流,基于内存的
		ObjectOutputStream ObjOut = null;
		ObjectInputStream ObjIn = null;
		
		try {
			ObjOut = new ObjectOutputStream(out);
			ObjOut.writeObject(girl);
			//必须写入以后才能执行下面这句话,否则对象没有写入流里面,读取的话会发生 EOFException
			ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
			ObjIn = new ObjectInputStream(in);
			Girl newGirl = (Girl) ObjIn.readObject();
			System.out.println(newGirl);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

运行截图:
在这里插入图片描述
说明:
对象的序列化,可以有很多种有意思的使用方法,此处我是将对象写入到内存种,然后再读取出来。没有写入到文件中,因为考虑CPU和磁盘和RAM的速度差异较大,还是在(RAM)内存中快一些。当然了,读者可以尝试从文件中序列化对象、从网络中序列化对象。这里强调序列化而不是深克隆是因为,深克隆是序列化的应用形式之一。

序列化的底层实现也是使用到了反射的知识,但是我还不是太明白,感兴趣可以去了解一下。

使用类加载器 搞对象

public class CreateObj {
	public static void main(String[] args) throws Exception {
		classLoader("伊人", "165", "110", "真可爱");
	}

	/**
	 * 类加载方式
	 * @throws MalformedURLException 
	 * */
	static void classLoader(String name, String length, String weight, String description) throws MalformedURLException {
		File file = null;
		file = new File("C:/Users/Alfred/Desktop/web/");
		URL url = file.toURI().toURL();  // file 的 toURL 已经废弃了,所以使用 toURI 然后再使用 toURL
		System.out.println(url.toString());
		URL[] urls = new URL[] {url};
		try (URLClassLoader classLoader = new URLClassLoader(urls)) {
			Class<?> clazz = classLoader.loadClass("obj.Girl");    // 应该根据类的全限定名搜索
			Constructor<?> con = clazz.getConstructor(String.class, String.class, String.class, String.class);
			Object obj = con.newInstance(name, length, weight, description);
			//因为此处的 obj 对象的类型是 obj.Girl 不是 dragon.obj.Girl
            //不直接使用此处 dragon.obj.Girl 的原因和 classpath 有关,
            //但是就算是 obj(Object) 也是可以调用方法的。
			//因为如果反射取到了对象,再转成相应的类型的话,这里是做不到的,因为这个类并不在当前程序的 classpath 中,
			//而且反射可以执行方法,也就没有必要了,(可能效率不高吧,但是我也没有写过大量使用反射的程序,对此不是很清楚。)
			System.out.println(obj);   
			// 这里单独调用一个方法
			Method method = clazz.getMethod("getDescription", new Class<?>[] {});
			String msg = (String) method.invoke(obj, new Object[] {});
			System.out.println("调用getDescription方法,执行结果为:" + msg);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

运行截图:
Javaer搞对象的多种方式_第1张图片

说明:
前面的方式都是很上层的方式,让我们了解一些底层的知识。Java源代码文件被编译之后,变成了class文件,我们运行时,运行的就是class文件。但是class文件是存放在磁盘上的,程序是运行在内存中的,所以就需要JVM将class文件读取到内存中,这种方式我们称之为:类加载。类加载本身是很复杂的,但是这里我们只要知道它可以把class文件加载到内存就行了。这里使用 URLClassLoader 类来加载一个不在classpath路径中的对象,如果是在classpath中的话(loadClass中的参数改成此程序的classpath中的类的全限定名:dragon.obj.Girl),似乎前面的urls参数就不重要了。这说明了什么,说明了它是先使用某种其它的类加载器尝试加载的,如果它无法加载,才会使用我们的类加载器加载的,这就是类加载的双亲委派机制。

注意:
我加载的是不在classpath中的一个类,它是 obj.Girl,不是 dragon.obj.Gril。虽然代码是一样的,但是它们是两个类,例如你是合肥的张三,我是芜湖的张三:合肥.张三 和 芜湖.张三 是不同的。当然了,我也就偷个懒,直接把 Gril.java 复制到某个文件夹(在此文件夹下新建名为 obj的文件夹,然后把源文件放入其中),然后修改包名为 package obj; 再编译一下即可。
Javaer搞对象的多种方式_第2张图片

自定义类加载器 搞对象

类加载器也是可以自定义的,用户完全可以自己实现一个简单的类加载器。这样可以自己添加更多丰富的操作,例如代码加密,对class文件使用特殊的加密方式,然后再在加载它的时候对其进行解密,从而实现包含源码(知识产权)。(因为Java的特殊机制,导致反编译是很方便的。)当然了,加密方式远不止这个,还有代码混淆等,但是这个是一个方向吧。

Java推荐的自定义类加载器的方式是继承 ClassLoader,然后重写 findClass 方法,下面提供一个自定义的简单的类加载器,注释部分有详细说明!

自定义类加载器 DragonClassLoader

package dragon.obj;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

public class DragonClassLoader extends ClassLoader {
	
	private String url; //需要加载的字节码文件的位置,绝对路径。
	
	public DragonClassLoader(String url) {
		this.url = url;
	}
	
	/**
	 * 自定义类加载器推荐重写此方法,比较安全,
	 * 重写 defineClass,对使用者能力要求较高。
	 * 
	 * 重写 findClass,以我个人的理解来说,就是根据 name,找到class文件的位置,
	 * 然后读取该文件的数据,将其作为参数传入 defineClass 即可。
	 * 当然了,还是需要自己动手,才能理解吧。
	 * */
	@Override
	public Class<?> findClass(String name) {
		name = name.replace('.', File.separatorChar);
		String filepath = this.url + name + ".class";
		System.out.println(filepath);
		
		//将该字节码文件读入内存中,这里我使用下面这个流来处理
		ByteArrayOutputStream output = new ByteArrayOutputStream();
		//开始读取文件
		try (InputStream input = new BufferedInputStream(new FileInputStream(filepath))) {
			int len = 0;
			byte[] b = new byte[1024];
			while ((len = input.read(b)) != -1) {
				output.write(b, 0, len);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		//将字节流转成字节数组
		byte[] data = output.toByteArray();
		return this.defineClass(null, data, 0, data.length);
	}
}

使用自定义类加载器加载,搞对象


public class CreateObj {
	public static void main(String[] args) throws Exception {
		defineClassLoader("伊人", "165", "110", "真可爱");
	}
	
	/**
	 * 使用自定义的类加载器创建对象
	 * */
	static void defineClassLoader(String name, String length, String weight, String description) {
		DragonClassLoader classLoader = new DragonClassLoader("C:/Users/Alfred/Desktop/web/");
		try {
			// 这里可以直接使用findClass,但是根据资料,似乎loadClass失败,
			// 会自动使用findClas的,符合双亲委派原则,双亲委派大致就是 爸爸能干的,就爸爸干,干不了再交给儿子干。(此处指加载类)
			Class<?> clazz = classLoader.loadClass("obj.Girl");  
			Constructor<?> con = clazz.getConstructor(String.class, String.class, String.class, String.class);
			Object obj = con.newInstance(name, length, weight, description);
			//因为此处的 obj 对象的类型是 obj.Girl 不是 dragon.obj.Girl
            //不直接使用此处 dragon.obj.Girl 的原因和 classpath 有关,
            //但是就算是 obj(Object) 也是可以调用方法的。
			//因为如果反射取到了对象,再转成相应的类型的话,这里是做不到的,因为这个类并不在当前程序的 classpath 中,
			//而且反射可以执行方法,也就没有必要了,(可能效率不高吧,但是我也没有写过大量使用反射的程序,对此不是很清楚。)
			System.out.println(obj);   
			// 这里单独调用一个方法
			Method method = clazz.getMethod("getDescription", new Class<?>[] {});
			String msg = (String) method.invoke(obj, new Object[] {});
			System.out.println("调用getDescription方法,执行结果为:" + msg);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

运行截图:
Javaer搞对象的多种方式_第3张图片
说明:
这也是一种方式,虽然平时编程的过程中可能用不到,毕竟这个都是属于不太常用的知识了。但是我们还是会经常遇到的,在各种框架中一般也都有类加载器的身影,足见它的重要性,还是很值得了解一下的。

总结 Summar

上面总结了好几种创建对象(搞对象)的方式,相信会对你有帮助,没有的话,也对我有帮助——帮助我复习了知识点,哈哈!不过就算会了这么多种方法,也还是没有搞到对象,这是该反思的。That’s a question!

你可能感兴趣的:(Java)