The Bean Class for JavaFX Programming
Let's check out the JavaFX bean class, see how it differs from Java beans, and learn when to use both in your applications.
Join the DZone community and get the full member experience.
Join For FreeIn my earlier article, The Bean Class for Java Programming, I presented how I would code a data or model class. That style of bean class is widely used in frameworks such as Hibernate, Java Server Faces, CDI, and others. There is one variant of that class that you need to know about and that is the JavaFX bean class.
JavaFX is the modern GUI framework that is the successor to Swing, just as it was the successor to AWT. One important characteristic of JavaFX is that the observable pattern is baked into its beans, pun intended. When a JavaFX bean is bound or connected to a JavaFX control, such as a text field, changes to the control can automatically update the bean and changes to the bean updates the control. This is an important feature in the Model View Controller pattern that JavaFX promotes.
A JavaFX bean, when properly constructed, can be a drop-in replacement for a Java bean in most instances. It is expected to adhere to all the mandatory, optional and even bonus features I described in the earlier article. Let’s begin by reviewing the fields from that article.
private String isbn;
private String title;
private String author;
private String publisher;
private int pages;
I have added an additional field to make this bean a little more interesting. The field will represent the date that the book was published.
private LocalDate datePublished;
To make this bean observable we must change how we declare these fields. JavaFX introduced a family of wrapper classes called property classes. These property classes exist for the primitive types, the String, List, Map, Object, Set, and a catch all for any other class type. These classes come in read/write as well as read only versions. They also come in abstract and concrete classes. The abstract class is the property type (LHS) and the concrete class is the instantiation (RHS). Here is a table of the read/write property classes.
Abstract / LHS Type Concrete / RHS Type
----------------------------------------------
BooleanProperty SimpleBooleanProperty
DoubleProperty SimpleDoubleProperty
FloatProperty SimpleFloatProperty
IntegerProperty SimpleIntegerProperty
ListProperty<E> SimpleListProperty<E>
LongProperty SimpleLongProperty
MapProperty<K,V> SimpleMapProperty<K,V>
ObjectProperty<T> SimpleObjectProperty<T>
SetProperty<E> SimpleSetProperty<E>
StringProperty SimpleStringProperty
The abstract property classes define the required methods of a property in a general way. The concrete class may override the general methods but do not add any additional methods. JavaFX has other concrete property classes that can appear on the right-hand side. The concrete classes extend the abstract class. This is an example of pure inheritance.
This means that we need to rewrite the fields as follows.
private final StringProperty isbn;
private final StringProperty title;
private final StringProperty author;
private final StringProperty publisher;
private final IntegerProperty pages;
private final ObjectProperty<LocalDate> datePublished;
The next change that we make from the original bean is that we now must have a constructor. These property fields are null references to abstract classes so you must instantiate them. If you do not plan to have a non-default constructor then all you need is something like this:
public BookFX() {
this.isbn = new SimpleStringProperty("");
this.title = new SimpleStringProperty("");
this.author = new SimpleStringProperty("");
this.publisher = new SimpleStringProperty("");
this.pages = new SimpleIntegerProperty(0);
this.datePublished = new SimpleObjectProperty<>(LocalDate.now());
}
Each of the references to an abstract class is instantiated with a concrete class. The concrete classes all accept an appropriate value of the underlying type in their constructor.
If we need to have both a default and non-default constructor they can be written like this:
public BookFX() {
this("", "", "", "", 0, LocalDate.now());
}
public BookFX(final String isbn, final String title, final String author, final String publisher, final int pages, final LocalDate datePublished) {
this.isbn = new SimpleStringProperty(isbn);
this.title = new SimpleStringProperty(title);
this.author = new SimpleStringProperty(author);
this.publisher = new SimpleStringProperty(publisher);
this.pages = new SimpleIntegerProperty(pages);
this.datePublished = new SimpleObjectProperty<>(datePublished);
}
The default constructor uses Java’s ability to have a constructor call another constructor by overloading. The non-default constructor can now be used to create an instance of the object and at the same time this class has the required default constructor.
The setters and getters must also be modified. These methods are expected to get or set the value inside the property object and not the property itself. This is easy to do as property classes have a get and set method to handle this. Here are the setters and getters.
public final String getIsbn() {
return isbn.get();
}
public void setIsbn(final String isbn) {
this.isbn.set(isbn);
}
public final String getTitle() {
return title.get();
}
public void setTitle(final String title) {
this.title.set(title);
}
public String getAuthor() {
return author.get();
}
public void setAuthor(final String author) {
this.author.set(author);
}
public final String getPublisher() {
return publisher.get();
}
public void setPublisher(final String publisher) {
this.publisher.set(publisher);
}
public final int getPages() {
return pages.get();
}
public void setPages(final int pages) {
this.pages.set(pages);
}
public final LocalDate getDatePublished() {
return datePublished.get();
}
public void setDatePublished(final LocalDate datePublished) {
this.datePublished.set(datePublished);
}
With this syntax, we now have a drop in replacement for standard Java beans as described in my first article. This is not complete yet. We are using property classes to achieve an observable pattern. To accomplish this JavaFX controls must be bound or connected to the individual fields. The controls need a reference to the property object. Only the equivalent of a getter is required. There is no naming convention because we write out the method name when we do the binding. The most common naming approach is to add the word Property to the variable name. Here are the getters for the properties.
public final StringProperty isbnProperty() {
return isbn;
}
public final StringProperty titleProperty() {
return title;
}
public final StringProperty authorProperty() {
return author;
}
public final StringProperty publisherProperty() {
return publisher;
}
public final IntegerProperty pagesProperty() {
return pages;
}
public final ObjectProperty<LocalDate> datePublishedProprty() {
return datePublished;
}
There is not a setter for a property because once created and bound to a control it must not be replaced. Only its contained value can be altered.
The hashCode, equals, toString and compareTo methods need to be modified to use the get and set methods of the properties.
@Override
public String toString() {
return "Book{" + "isbn=" + isbn.get() + ", title=" + title.get() + ", author=" + author.get() + ", publisher=" + publisher.get() + ", pages=" + pages.get() + ", datePublished=" + datePublished.get() + '}';
}
@Override
public int hashCode() {
int hash = 7;
hash = 61 * hash + Objects.hashCode(this.isbn.get());
hash = 61 * hash + Objects.hashCode(this.title.get());
hash = 61 * hash + Objects.hashCode(this.author.get());
hash = 61 * hash + Objects.hashCode(this.publisher.get());
hash = 61 * hash + this.pages.get();
hash = 61 * hash + Objects.hashCode(this.datePublished.get());
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final BookFX other = (BookFX) obj;
if (this.pages.get() != other.pages.get()) {
return false;
}
if (!Objects.equals(this.isbn.get(), other.isbn.get())) {
return false;
}
if (!Objects.equals(this.title.get(), other.title.get())) {
return false;
}
if (!Objects.equals(this.author.get(), other.author.get())) {
return false;
}
if (!Objects.equals(this.publisher.get(), other.publisher.get())) {
return false;
}
if (!Objects.equals(this.datePublished.get(), other.datePublished.get())) {
return false;
}
return true;
}
@Override
public int compareTo(BookFX book) {
return this.title.get().compareTo(book.title.get());
}
The only change required to our Comparator class is to declare the beans as JavaFX beans rather than Java beans.
public class BookPageComparatorFX implements Comparator<BookFX> {
@Override
public int compare(BookFX book1, BookFX book2) {
return book1.getPages() - book2.getPages();
}
}
Put all together this is what our JavaFX bean looks like.
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Objects;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class BookFX implements Serializable, Comparable<BookFX> {
private final StringProperty isbn;
private final StringProperty title;
private final StringProperty author;
private final StringProperty publisher;
private final IntegerProperty pages;
private final ObjectProperty<LocalDate> datePublished;
/**
* Default constructor
*/
public BookFX() {
this("", "", "", "", 0, LocalDate.now());
}
/**
* Non-default constructor
*
* @param isbn
* @param title
* @param author
* @param publisher
* @param pages
* @param datePublished
*/
public BookFX(final String isbn, final String title, final String author, final String publisher, final int pages, final LocalDate datePublished) {
this.isbn = new SimpleStringProperty(isbn);
this.title = new SimpleStringProperty(title);
this.author = new SimpleStringProperty(author);
this.publisher = new SimpleStringProperty(publisher);
this.pages = new SimpleIntegerProperty(pages);
this.datePublished = new SimpleObjectProperty<>(datePublished);
}
public final String getIsbn() {
return isbn.get();
}
public void setIsbn(final String isbn) {
this.isbn.set(isbn);
}
public final StringProperty isbnProperty() {
return isbn;
}
public final String getTitle() {
return title.get();
}
public void setTitle(final String title) {
this.title.set(title);
}
public final StringProperty titleProperty() {
return title;
}
public String getAuthor() {
return author.get();
}
public void setAuthor(final String author) {
this.author.set(author);
}
public final StringProperty authorProperty() {
return author;
}
public final String getPublisher() {
return publisher.get();
}
public void setPublisher(final String publisher) {
this.publisher.set(publisher);
}
public final StringProperty publisherProperty() {
return publisher;
}
public final int getPages() {
return pages.get();
}
public void setPages(final int pages) {
this.pages.set(pages);
}
public final IntegerProperty pagesProperty() {
return pages;
}
public final LocalDate getDatePublished() {
return datePublished.get();
}
public void setDatePublished(final LocalDate datePublished) {
this.datePublished.set(datePublished);
}
public final ObjectProperty<LocalDate> datePublishedProprty() {
return datePublished;
}
@Override
public String toString() {
return "Book{" + "isbn=" + isbn.get() + ", title=" + title.get() + ", author=" + author.get() + ", publisher=" + publisher.get() + ", pages=" + pages.get() + ", datePublished=" + datePublished.get() + '}';
}
@Override
public int hashCode() {
int hash = 7;
hash = 61 * hash + Objects.hashCode(this.isbn.get());
hash = 61 * hash + Objects.hashCode(this.title.get());
hash = 61 * hash + Objects.hashCode(this.author.get());
hash = 61 * hash + Objects.hashCode(this.publisher.get());
hash = 61 * hash + this.pages.get();
hash = 61 * hash + Objects.hashCode(this.datePublished.get());
return hash;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final BookFX other = (BookFX) obj;
if (this.pages.get() != other.pages.get()) {
return false;
}
if (!Objects.equals(this.isbn.get(), other.isbn.get())) {
return false;
}
if (!Objects.equals(this.title.get(), other.title.get())) {
return false;
}
if (!Objects.equals(this.author.get(), other.author.get())) {
return false;
}
if (!Objects.equals(this.publisher.get(), other.publisher.get())) {
return false;
}
if (!Objects.equals(this.datePublished.get(), other.datePublished.get())) {
return false;
}
return true;
}
/**
* Natural comparison method for Title
*
* @param book
* @return negative value, 0, or positive value
*/
@Override
public int compareTo(BookFX book) {
return this.title.get().compareTo(book.title.get());
}
}
The test program for this bean is almost identical to the original BeanTester but with the LocalDate added and used in a Comparator function.
// Required for Arrays.sort
import java.time.LocalDate;
import java.util.Arrays;
// Required by the Comparator function
import static java.util.Comparator.comparing;
public class BeanTesterFX {
/**
* Here is where I am testing my comparisons
*
*/
public void perform() {
// Lets create four books
BookFX b0 = new BookFX("200", "Xenon", "Hamilton", "Harcourt", 99, LocalDate.of(2017, 2, 4));
BookFX b1 = new BookFX("500", "Boron", "Bradbury", "Prentice", 108, LocalDate.of(2018, 5, 23));
BookFX b2 = new BookFX("300", "Radon", "Heinlein", "Thompson", 98, LocalDate.of(2015, 8, 30));
BookFX b3 = new BookFX("404", "Argon", "Campbell", "Hachette", 102, LocalDate.of(2012, 11, 15));
// Using Comparable to compare two books
System.out.println("Value returned by Comparable");
System.out.println(b0.getTitle() + " compared to " + b1.getTitle() + ": "
+ b0.compareTo(b1));
System.out.println();
// Using Comparator to compare two books
System.out.println("Value returned by Comparator");
BookPageComparatorFX bookPageComparatorFX = new BookPageComparatorFX();
System.out.println(b0.getPages() + " compared to " + b1.getPages() + ": "
+ bookPageComparatorFX.compare(b0, b1));
System.out.println();
// Create an array we can sort
BookFX[] myBooks = new BookFX[4];
myBooks[0] = b0;
myBooks[1] = b1;
myBooks[2] = b2;
myBooks[3] = b3;
System.out.println("Unsorted");
displayBooks(myBooks);
System.out.println("Sorted with Comparable Interface on Title");
Arrays.sort(myBooks); // uses the Comparable compareTo in the bean
displayBooks(myBooks);
System.out.println("Sorted with Comparable Object on Pages");
Arrays.sort(myBooks, bookPageComparatorFX); // uses the Comparator object
displayBooks(myBooks);
System.out.println("Sorted with Comparable lambda expression on Publishers");
Arrays.sort(myBooks, (s1, s2) -> {
return s1.getPublisher().compareTo(s2.getPublisher());
}); // uses the Comparator lambda
displayBooks(myBooks);
System.out.println("Sorted with Comparable lambda functions based on ISBN");
Arrays.sort(myBooks, comparing(BookFX::getIsbn)); // Comparable function
displayBooks(myBooks);
System.out.println("Sorted with Comparable lambda functions based on Date Published");
Arrays.sort(myBooks, comparing(BookFX::getDatePublished)); // Comparable function
displayBooks(myBooks);
}
/**
* Print the contents of each Book object in the array
*
* @param theBooks
*/
private void displayBooks(BookFX[] theBooks) {
for (BookFX b : theBooks) {
System.out.print(b.getIsbn() + "\t");
System.out.print(b.getTitle() + "\t");
System.out.print(b.getAuthor() + "\t");
System.out.print(b.getPublisher() + "\t");
System.out.println(b.getPages() + "\t");
System.out.println(b.getDatePublished());
}
System.out.println();
}
/**
* Where it all begins
*
* @param args
*/
public static void main(String[] args) {
BeanTesterFX bt = new BeanTesterFX();
bt.perform();
System.exit(0);
}
}
JavaFX does require that you use JavaFX beans as described here. They are only required if you intend to take advantage of binding to controls. If binding is not required, then an ordinary Java bean should be used.
To summarize these two articles, if you are not using JavaFX, then you should use the Java bean class for data and models. In a JavaFX application, you should use the JavaFX bean whenever binding is required.
Published at DZone with permission of Ken Fogel, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments