为什么我应该避免在 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 应用程序开发人员必须在他们想要设置 TableColumn
的 cellValueFactory
时创建匿名 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 有其自身的缺点。这些缺点是:
- 依靠reflection,以及
- 失去 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会有两个问题。
PropertyValueFactory
将无法找到“名称”属性 并将在 run-time 处抛出异常。这是因为 Person
的方法不遵循 属性 的命名约定。在这种情况下,他们没有遵循 camelCase
模式。方法应该是:
getname
→getName
setname
→setName
nameproperty
→nameProperty
修复此问题将修复此错误,但随后您 运行 进入第二个问题。
对 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 表达式甚至给了你更多的自由,比如能够使用表达式绑定。
缺点
有一个缺点,虽然很小。
“数字属性”,如IntegerProperty
和DoubleProperty
,都实现了ObservableValue<Number>
。这意味着您要么必须:
使用 Number
而不是 Integer
作为列的值类型。这还不错,因为您可以根据需要调用例如 Number#intValue()
。
或使用例如 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 的示例:
许多与 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 应用程序开发人员必须在他们想要设置 TableColumn
的 cellValueFactory
时创建匿名 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 有其自身的缺点。这些缺点是:
- 依靠reflection,以及
- 失去 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会有两个问题。
PropertyValueFactory
将无法找到“名称”属性 并将在 run-time 处抛出异常。这是因为Person
的方法不遵循 属性 的命名约定。在这种情况下,他们没有遵循camelCase
模式。方法应该是:getname
→getName
setname
→setName
nameproperty
→nameProperty
修复此问题将修复此错误,但随后您 运行 进入第二个问题。
对
时,我们“不小心”(在这个人为的示例中)创建了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 表达式甚至给了你更多的自由,比如能够使用表达式绑定。
缺点
有一个缺点,虽然很小。
“数字属性”,如IntegerProperty
和DoubleProperty
,都实现了ObservableValue<Number>
。这意味着您要么必须:
使用
Number
而不是Integer
作为列的值类型。这还不错,因为您可以根据需要调用例如Number#intValue()
。或使用例如
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 的示例: