使用反射来使用特定的类构造函数并创建新对象



我有两个类。

public class Shape1 extends javafx.scene.shape.Path {
public Shape1(PathElement... elements) {
super(elements);
}    
}
public class Shape2 extends javafx.scene.shape.Path {
public Shape2(PathElement... elements) {
super(elements);
}    
}

它们不一样。它们有不同的领域和方法,但为了简单起见,我没有提及它们

我为每种类型的对象创建两个数组,并且只需要使用一种方法来创建它们。

ArrayList<Shape1> first_shapes = create_array(50, 50, 100, 100, Class.forName("example3.Shape1"));
ArrayList<Shape2> second_shapes = create_array(50, 100, 100, 150, Class.forName("example3.Shape2"));
public static ArrayList create_array(int X1, int Y1, int X2, int Y2, Class my_class) {
var shapes = new ArrayList<>();
shapes.add(my_class.getConstructor(PathElement[].class).newInstance(new MoveTo(X1, Y1), new LineTo(X2, Y2)));
...
return shapes;
}

代码有两个问题。

  1. 类定义不合适,因为当您更改类名或包名时,它将不起作用,并且必须手动更改。

  2. 实际上,它根本不起作用,因为当您向数组中添加对象时,它会崩溃。

奇怪的是,在上面的表格中,它会显示一个错误。

参数数量错误

但如果我只使用一个参数,

shapes.add(my_class.getConstructor(PathElement[].class).newInstance(new MoveTo(X1, Y1)));

它将显示一个错误。

参数类型不匹配

这是解决我问题的唯一办法。否则我将不得不改变整个算法。

请帮助

谢谢

首先,启用所有编译器警告,并注意它们。您永远不希望在没有泛型类型的情况下使用ClassArrayList;请参阅什么是原始类型以及为什么不应该';我们不用它吗?。

关于反思的最佳策略是避免使用反思。

  • 反射很慢。它不太可能被即时编译器优化
  • 反射是不安全的。编译器无法检查您是否犯了错误,例如拼写错误或方法签名错误
  • 反思很难读懂。您总是需要能够在以后的某个日期理解和维护您的代码。在专业的世界里,其他人可能需要在你离开后很长一段时间内维护你的代码

不要使用反射,而是将每个构造函数视为一个函数。具体来说,一个函数,它将一系列PathElement作为输入,并生成一个特定类的新实例作为输出。在Java中,它被写成:

Function<PathElement[], S>

其中S本身定义为<S extends Path>,即从Path继承而来的已知类型。

您可以将这个已知类型和已知构造函数传递给您的方法:

public static <S extends Path> ArrayList<S> create_array(
int X1, int Y1, int X2, int Y2,
Function<PathElement[], S> shapeConstructor) {
var shapes = new ArrayList<S>();
shapes.add(shapeConstructor.apply(new PathElement[] {
new MoveTo(X1, Y1), new LineTo(X2, Y2)
}));
//...
return shapes;
}

现在,您拥有了类型安全的代码,并且能够在编译时捕获拼写或语法中的任何错误。

该方法的调用如下所示:

ArrayList<Shape1> shapeList1 = create_array(10, 10, 50, 50, Shape1::new);
ArrayList<Shape2> shapeList2 = create_array(20, 20, 80, 80, Shape2::new);
  1. 问题:使用Shape1.class而不是Class.forName("example3.Shape1")。通过这种方式,通过标准的重命名重构来更改pkg和类名,可以在任何地方更改它。Class.forName()是类解析所必需的,您无法预测,它是动态的,例如来自用户的输入。您只需要对Class<Shape1>实例的静态Shape1.class引用

此外,更改方法参数以接受通用Class<T> myClass并返回相同类型T的列表,如下所示:

public static <T> ArrayList<T> create_array(int X1, int Y1, int X2, int Y2, Class<T> my_class) { ... }

这样,它对泛型是类型安全的,而不会对原始类型发出警告。

  1. 如果它需要一个数组参数,那么您应该将单个参数封装在一个数组中

参数数量错误意味着调用构造函数时需要一个实际有2个参数的数组参数。显然,Varargs不会自动通过反射封装在数组中。

参数类型不匹配:同样,它需要一个数组PathElement[],但您只提供PathElement


替代解决方案

由于各种原因,反思在大多数情况下是最后的手段。在这种情况下,使用反射真的有必要吗?你可以通过继承来解决这个问题。当然,它们都已经扩展了一个超类&有不同的字段和方法,但您可以创建一个几乎为空的中间abstract class BaseShape,它将包含您的方法(除了Class参数ofc(和使用的构造函数-它将被继承。

从OOP的角度来看,这绝对是一个比使用反射的静态方法更干净的解决方案。