JAVA反射练习

JAVA反射练习

题目

实现一个方法
public static Object execute(String className, String methodName, Object args[])
实现 “通过类的名字、方法名字、方法参数调调用方法,返回值为该方法的返回值。” 的功能。

解题思路

开始阶段

  一开始看到这个题目,以为很简单。大致思路就是通过反射获取字节码文件对象,然后该对象获取方法名的方法对象。
  将args数组转换成Class对象数组,这样来获取具体的调用某一个方法,最后调用invoke(obj,args)方法完成。

发现问题

按照这个思路写出程序,如下所示:

    public static Object execute(String className, String methodName,Object[] args) throws Exception {
        // 获取类的字节码文件对象
        Class cls = Class.forName(className);
        // 获取方法调用的参数的Class对象
        Class[] paramsCls = new Class[args.length];
        for (int i = 0; i < args.length; i++) {
            paramsCls[i] = args[i].getClass();
        }
        // 获取方法对象
        Method method = cls.getMethod(methodName, paramsCls);
        Object obj = cls.newInstance();
        return method.invoke(obj,args);
    }

  乍一看,这个程序没有什么太大的问题,可是在测试这个方法时出现了问题,如测试一个public int add(int a,int b)。在传递参数时,是一个Object类型的数组,如20,30这两个参数在传递过去时就被自动的装箱成为了Integer类型。那么获取的Class对象就成为了 class java.lang.Integer,但是add方法的参数却是int类型。
  一开始我以为没什么问题,但是在调用cls.getMethod()方法时,出现了问题。虚拟机抛出异常,表示没有这样的方法,这让我很困惑,后来百度了一下,发现int.class和Integer.class并不能混为一谈,也就是这两者并不能像自动拆装箱那样进行转换。也明白了Class类的getMethod(methodName,paramTypes)中paramTypes需要的Class类型就是方法定义时的数据类型,add(int a,int b)参数是int,那么getMethod时传递的就是int.class,如果传递一个Integer.class,并不能获取到这个方法。所以这个程序并不是特别完善。

思考

  1. 解决方案1
      既然基本数据类型在作为参数传递时,都变成了各自的包装类,那么只要在定义方法时,方法参数不允许使用基本数据类型就可以了。
  2. 解决方案2
      在获取类的Class对象后,去获取类的所有方法,然后遍历获取的方法对象,拿方法对象名与methodName去比较,若相同,则调用该方法。

大致的解决思路就是这样,后来仔细思考了一下,这两种解决方案都不是特别完美。

  1. 方案1的缺陷
      程序的通用性很差,如果我使用这个方法去调用别人定义的类中的方法,假如其他人在定义方法时,并没有将基本数据类型写成包装类,那么我使用这个方法就会发生错误,不能正确的执行。
  2. 方案2的缺陷
      方案2使用循环遍历Methods数组,并通过methodName来比较判断是不是调用的该方法。但是假如我碰到了方法的重载,如调用add(int a, int b)时,这个类里面还有add(int a,int b, int c),或者add(double a,double b)。这就是三个方法, 并且方法的名称都是"add",假如我传递过来的methodName是"add",args是{20,30},那么在循环遍历时,就会出现问题,如首先遍历的是add(int a,int b, int c),那么在invoke时就会发生错误。所以方案2还需要判断方法的重载问题。

最终解决方案

  我的最终解决方案就是在方案2的基础上,再对程序进行完善。那就是在判断method对象时,多追加几个判断条件。
  即:当方法名一样时,判断传递的参数个数是否与当前方法对象所需要的参数个数相同,若相同,则再判断每一个参数类型是否与方法所需要的类型是否一致。这样就能精确的定位调用哪一个方法了。
完善后程序如下:

    public static Object execute(String className, String methodName, Object args[]) throws Exception {
		Class cls = Class.forName(className);
		Method[] methods = cls.getMethods();
		Object obj = cls.newInstance();
		for (Method method : methods) {
			// 获取方法所需要参数的Class对象数组
			Class[] types = method.getParameterTypes();
			// 判断methodName是否和方法名一致,若一致,再判断传递的参数个数是否一致。参数个数一致后再判断参数类型是否一致
			if (method.getName().equals(methodName) && args.length == types.length
					&& isEqualParamAndTypes(args, types)) {
				// 都一致 执行该方法
				return method.invoke(obj, args);
			}
		}
		System.out.println("没有这个方法或参数不匹配");
		return null;
	}
	
	/**
	 * 判断参数数组的类型是否与方法所需要的参数类型是否一致
	 * @param args 方法调用参数
	 * @param types 方法所需要的参数类型
	 * @return true代表一致,false不一致
	 */
	private static boolean isEqualParamAndTypes(Object[] args, Class[] types) {
		boolean flag = false;
		for (int i = 0; i < args.length; i++) {
			String clsName = args[i].getClass().toString();
			String typeName = types[i].toString();
			// 上面获取参数的Class对象的字符串表示形式,是为了更好的去判断参数是否为基本数据类型。
			// 这里还需要去判断方法参数是否为基本数据类型。 如果是,那么照样是可以通过的
			if (clsName.equals(typeName) || isBasicType(clsName).equals(typeName)) {
				flag = true;
			} else {
				flag = false;
				break;
			}
		}
		return flag;
	}
	
	/**
	 * 判断字节码文件对象的字符串表示形式是否为基本数据类型的包装类型,若是,则返回基本数据类型的class对象的字符串表示形式
	 * @param clsName 字节码文件(Class)对象的字符串表示形式
	 * @return 若是包装类型,返回对应类型的基本数据类型的class对象表示形式,若不是,则返回该字符串本身
	 */
	private static String isBasicType(String clsName) {
		switch (clsName) {
		case "class java.lang.Byte":
			return "byte";
		case "class java.lang.Short":
			return "short";
		case "class java.lang.Integer":
			return "int";
		case "class java.lang.Long":
			return "long";
		case "class java.lang.Float":
			return "float";
		case "class java.lang.Double":
			return "double";
		case "class java.lang.Character":
			return "char";
		case "class java.lang.Boolean":
			return "boolean";
		default:
			return clsName;
		}
	}

结果: 能够成功的运行,并且能够很好的区分方法重载问题。

总结

  这个练习题看似简单,但还是有不少坑是可以值得挖一挖的。
  一开始我使用的是办法类似于方案1,在获取参数的Class对象时,判断一下是否为包装类型,若是,则转换成对应的基本类型的class。
判断条件:

    if(args[i].getClass().getSimpleName().equals("Integer")){
        cls[i] = int.class;
    }else{
        cls[i] = args[i].getClass();
    }

  这样转换会出现一个问题,当我指定一个方法的参数类型为Integer时,上面的判断条件又会将Integer类型转换为int.class,这样在getMethod(methodName,Class[] clsType)又会出现异常。这样使得在调用时必须确定方法的参数类型的class对象,又由于int.class和Integer.class不一样,上面的判断条件无法精确的判断方法的参数到底是int还是Integer类型。就很令人头痛。
  于是我就想到假如我获取到所有的方法对象,并遍历每一个方法对象,判断传递的方法名称是否和遍历的方法名一致,这样我不就明确的知道了方法所需要的参数个数和参数的具体类型了吗。
  知道了方法名 然后再通过传递的参数与方法所需要的参数进行匹配。一致,则调用该方法,不一致则不调用。
  这就是方案2的思路,但是在判断类型时,我使用的是Class对象的字符串表示形式去判断而不是直接使用Class对象去判断,因为这样能够使用switch()判断,因为switch只接收基本数据类型和字符串类型的,而Class对象不在此范围内。
   要是直接使用Class对象去比较判断的话,也不好解决。比如:获取到了参数的Class对象为Integer.class,判断时就是 if(cls == Integer.class){ cls = int.class} 这样就出现了上面一样的错误,假如方法参数要的是Integer.class 而上边判断又转成了int.class,这样不符合条件,就跳过了这一方法,这样一来方法就无法执行了。所以使用字符串比较就比较合理了,也就是如下的判断条件:

    if (clsName.equals(typeName) || isBasicType(clsName).equals(typeName)) {
        flag = true;
    } else {
        flag = false;
        break;
    }

   这样判断的好处就是: 当传递的参数的Class对象字符串与方法需要的字符相同时,就符合条件。
当不一样时,再接着判断方法所需的参数是否是一个基本类型。若是,则符合条件。
   这个程序大致就是这个样子了,但仍然还存在一些缺陷。如:当传递的参数是数组时,又出现了基本类型数组与包装类型数组的区别。这又需要去判断一下了。这里我就没有再过多的去完善它了。
  通过写这个反射程序,让我不禁感慨写出一个健壮性很强的程序是多么的费事。更让我明白还有更多的知识等待着我去学习。
  如果有更好的方法来实现这个功能,欢迎在评论中贴出来。

posted @ 2019-03-24 17:18  幽林绿野  阅读(1424)  评论(0编辑  收藏  举报