为什么我应该避免在 JavaFX 中使用 PropertyValueFactory,我应该改用什么?

Why should I avoid using PropertyValueFactory in JavaFX, and what should I use instead?

许多与 PropertyValueFactory 相关的问题的答案(和评论)建议避免 class 和其他喜欢它的人。使用这个 class 有什么问题,应该用什么代替它?

TL;DR:

  • 你应该避免 PropertyValueFactory 和类似的 classes 因为它们依赖于反射,更重要的是,会导致你失去有用的 compile-time 验证(例如如果 属性 实际存在)。

  • PropertyValueFactory 的使用替换为 lambda 表达式。例如,替换:

    nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
    

    有:

    nameColumn.setCellValueFactory(data -> data.getValue().nameProperty());
    

    (假设您正在使用 Java 8+ 并且您已经定义了模型 class 以公开 JavaFX 属性)


PropertyValueFactory

这个 class 和其他类似的东西,是一种方便 class。 JavaFX 是在 Java 7 时代(如果不是更早的话)发布的。那时,lambda 表达式还不是语言的一部分。这意味着 JavaFX 应用程序开发人员必须在他们想要设置 TableColumncellValueFactory 时创建匿名 class。它看起来像这样:

// Where 'nameColumn' is a TableColumn<Person, String> and Person has a "name" property
nameColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Person>, ObservableValue<String>>() {

  @Override
  public ObservableValue<String> call(TableColumn.CellDataFeatures<Person> data) {
    return data.getValue().nameProperty();
  }
});

如您所见,这非常冗长。想象一下对 5 列、10 列或更多列执行相同的操作。因此,JavaFX 的开发人员添加了方便的 classes,例如 PropertyValueFactory,允许将上面的内容替换为:

nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));

PropertyValueFactory 的缺点

但是,使用 PropertyValueFactory 和类似的 classes 有其自身的缺点。这些缺点是:

  1. 依靠reflection,以及
  2. 失去 compile-time 验证。

反思

这是两个缺点中较小的一个,但它直接导致第二个缺点。

PropertyValueFactory 将 属性 的名称作为 String。然后它可以调用模型 class 的方法的唯一方法是通过反射。你应该尽可能避免依赖反射,因为它增加了一个间接层并减慢了速度(尽管在这种情况下,性能损失可能可以忽略不计)。

反射的使用还意味着您必须依赖编译器无法强制执行的约定。在这种情况下,如果您不完全遵循 JavaFX 属性 的命名约定,那么实现将无法找到所需的方法,即使您认为它们存在。

否 Compile-time 验证

由于PropertyValueFactory 依赖于反射,Java 只能在run-time 验证某些事情。更具体地说,编译器无法在编译期间验证 属性 是否存在,或者 属性 是否是正确的类型。这使得开发代码更加困难。

假设您有以下模型 class:

/*
 * NOTE: This class is *structurally* correct, but the method names
 *       are purposefully incorrect in order to demonstrate the
 *       disadvantages of PropertyValueFactory. For the correct
 *       method names, see the code comments above the methods.
 */
public class Person {

  private final StringProperty name = new SimpleStringProperty(this, "name");

  // Should be named "setName" to follow JavaFX property naming conventions
  public final void setname(String name) {
    this.name.set(name);
  }
 
  // Should be named "getName" to follow JavaFX property naming conventions
  public final String getname() {
    return name.get();
  }

  // Should be named "nameProperty" to follow JavaFX property naming conventions
  public final StringProperty nameproperty() {
    return name;
  }
}

有这样的东西编译就好了:

TableColumn<Person, Integer> nameColumn = new TableColumn<>("Name");
nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
nameColumn.setCellFactory(tc -> new TableCell<>() {

  @Override 
  public void updateItem(Integer item, boolean empty) {
    if (empty || item == null) {
      setText(null);
    } else {
      setText(item.toString());
    }
  }
});

但是在run-time会有两个问题。

  1. PropertyValueFactory 将无法找到“名称”属性 并将在 run-time 处抛出异常。这是因为 Person 的方法不遵循 属性 的命名约定。在这种情况下,他们没有遵循 camelCase 模式。方法应该是:

    • getnamegetName
    • setnamesetName
    • namepropertynameProperty

    修复此问题将修复此错误,但随后您 运行 进入第二个问题。

  2. updateItem(Integer item, boolean empty) 的调用将抛出 ClassCastException,表示 String 无法转换为 Integer。当我们应该创建 TableColumn<Person, String>.

    时,我们“不小心”(在这个人为的示例中)创建了 TableColumn<Person, Integer>

您应该改用什么?

您应该使用 lambda 表达式替换 PropertyValueFactory 的使用,lambda 表达式已添加到版本 8 中的 Java 语言中。

由于Callback是一个函数式接口,它可以用作lambda表达式的目标。这允许你这样写:

// Where 'nameColumn' is a TableColumn<Person, String> and Person has a "name" property
nameColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Person>, ObservableValue<String>>() {

  @Override
  public ObservableValue<String> call(TableColumn.CellDataFeatures<Person> data) {
    return data.getValue().nameProperty();
  }
});

像这样:

nameColumn.setCellValueFactory(data -> data.getValue().nameProperty());

这基本上与 PropertyValueFactory 方法一样简洁,但没有上面讨论的任何缺点。例如,如果您忘记定义 Person#nameProperty(),或者如果它没有 return 和 ObservableValue<String>,那么将在 compile-time 处检测到错误。这迫使您在应用程序可以 运行.

之前解决问题

lambda 表达式甚至给了你更多的自由,比如能够使用表达式绑定。

缺点

有一个缺点,虽然很小。

“数字属性”,如IntegerPropertyDoubleProperty,都实现了ObservableValue<Number>。这意味着您要么必须:

  1. 使用 Number 而不是 Integer 作为列的值类型。这还不错,因为您可以根据需要调用例如 Number#intValue()

  2. 或使用例如 IntegerProperty#asObject(),其中 return 是 ObjectProperty<Integer>。其他“数字属性”也有类似的方法。

    column.setCellValueFactory(data -> data.getValue().someIntegerProperty().asObject());
    

科特林

如果您使用的是 Kotlin,则 lambda 可能如下所示:

nameColumn.setCellValueFactory { it.value.nameProperty }

假设您在模型中定义了适当的 Kotlin 属性 class。有关详细信息,请参阅 this Stack Overflow answer

记录

如果数据是你的TableView是read-only那么你可以使用record,这是一种特殊的class.

对于记录,您不能使用 PropertyValueFactory 并且必须使用自定义单元格值工厂(例如 lambda)。

记录访问器方法的命名策略不同于标准 java bean 命名策略。例如,对于名为 name 的成员,PropertyValueFactory 使用的标准 java beans 访问器名称将是 getName(),但对于记录,[=58= 的访问器] 成员只是name()。因为记录不遵循 PropertyValueFactory 要求的命名约定,所以 PropertyValueFactory 不能用于访问存储在记录中的数据。

但是,此答案中详述的 lambda 方法将能够很好地访问记录中的数据。

可以在以下位置找到更多信息和将带有单元格值工厂的记录用于 TableView 的示例: