Working with Data Models
Data models are the foundation of your application's data. They represent the information your application works with, independent of how that information is displayed or manipulated in the UI. In the Jukebox project, the model classes form a hierarchy of data that represents albums, songs, users, and genres.
- Simple model classes (Album, Song, User)
- Enum types for fixed sets of values (Genre)
- Composition - objects containing other objects
- Public fields vs private fields with getters/setters
- The toString() method for display purposes
- The Database class as a container for all data
The Model Classes
The Jukebox project has several model classes that work together to represent the application's data:
The Album Class
Album.java is a model class that represents a music album. Let's examine it in detail:
package jukebox.model;
import java.util.ArrayList;
import java.util.List;
public class Album {
public String name = "";
public String artist = "";
public Genre genre = Genre.UNKNOWN;
public List<Song> songs = new ArrayList<>();
public String toString() {
return name + " by " + artist;
}
}
- Public Fields: All fields are public, which makes them easy to access and modify from controllers. For production code, consider using private fields with getters and setters.
- Default Values: All fields have default values (empty string, UNKNOWN genre, empty list)
- Composition: An Album contains a List of Song objects - this is composition, where one object contains other objects
- toString() Method: Provides a human-readable string representation used by ListView
The Song Class
Song.java represents an individual song/track:
package jukebox.model;
public class Song {
public String title = "";
public int lengthSeconds = 0;
public String toString() {
return title;
}
}
- Simple Structure: Only two fields - title and length in seconds
- lengthSeconds: Stored as an integer representing total seconds (e.g., 210 for 3:30)
- toString() Method: Returns just the title, which is what ListView displays
Note: For display purposes, you might want to format the length as "minutes:seconds". This formatting would typically be done in the controller, not in the model.
The Genre Enum
Genre.java defines a fixed set of music genres using a Java enum:
package jukebox.model;
public enum Genre {
ROCK,
HIP_HOP,
COUNTRY,
UNKNOWN
}
- Type Safety: Compiler ensures only valid genre values are used
- Readability: More readable than numeric codes or strings
- Maintainability: Easy to add new genres if needed
- Performance: Enums are efficient (internally they're like integers)
Usage in AdminPanelController: The genre ComboBox is populated with all Genre enum values.
The User Class
User.java represents the application user:
package jukebox.model;
public class User {
public String username = "";
}
Currently, the User class is very simple, storing only a username. In a more complete application, it might store:
- User preferences
- Play history
- Favorite albums/songs
- Login credentials
- Other profile information
The Database Class
Database.java is the main container that holds all application data. It also handles persistence (saving and loading):
package jukebox.model;
import com.google.gson.Gson;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class Database {
public User user = new User();
public List<Album> albums = new ArrayList<>();
// Persistence methods
public void saveData() { ... }
public void loadData() { ... }
}
- Central Data Store: Contains all application data in one place
- Simple Structure: Public fields for user and albums list
- Persistence: Methods to save and load data from JSON
- Singleton-like: One instance is created and shared throughout the application
Model Relationships
The model classes have the following relationships:
┌─────────────────┐
│ Database │
│─────────────────│
│ + user: User │
│ + albums: List │
│ <Album> │
└────────┬────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐
│ Album │ │ User │
│─────────────────│ │─────────────────│
│ + name: String │ │ + username: │
│ + artist: String │ │ String │
│ + genre: Genre │ └─────────────────┘
│ + songs: List │
│ <Song> │
└────────┬────────┘
│
▼
┌─────────────────┐
│ Song │
│─────────────────│
│ + title: String │
│ + lengthSeconds:│
│ int │
└─────────────────┘
Databasehas-aUserand a List ofAlbumsAlbumhas-a List ofSongsSongis a simple data class with no child objectsUseris a simple data class with no child objectsGenreis an enum used by Album
This is a classic composition hierarchy, where complex objects are built from simpler ones.
toString() Method Importance
Notice that both Album and Song override the toString() method. This is important because:
- ListView Display: When you add objects to a ListView without a custom cell factory, JavaFX calls toString() to display them
- Debugging: Helpful for debugging and logging
- ComboBox Display: ComboBox also uses toString() by default
- TableView (indirect): While TableView uses cell value factories, toString() is still useful for debugging
Example: In AdminPanelController, the albums ListView uses Album.toString() by default:
// In AdminPanelController.java
public void initialize() {
albums = FXCollections.observableArrayList(Jukebox.database.albums);
albumsList.setItems(albums);
// ListView will display each Album using its toString() method
}
Matching FXML:
<ListView fx:id="albumsList" prefHeight="200" prefWidth="300" />
Result: Each album appears as "Album Name by Artist Name" in the ListView.
Public Fields vs Private with Getters/Setters
The Jukebox project uses public fields for simplicity. This makes the code easier to follow for beginners. However, for production code, there are good reasons to use private fields with getters and setters:
Example with Getters/Setters:
public class Album {
private String name;
private String artist;
private Genre genre;
private List<Song> songs = new ArrayList<>();
// Getters
public String getName() { return name; }
public String getArtist() { return artist; }
public Genre getGenre() { return genre; }
public List<Song> getSongs() { return songs; }
// Setters
public void setName(String name) { this.name = name; }
public void setArtist(String artist) { this.artist = artist; }
public void setGenre(Genre genre) { this.genre = genre; }
public void setSongs(List<Song> songs) { this.songs = songs; }
}
Gson can serialize both public fields and private fields with getters (if the getter follows the JavaBean convention). So using private fields with getters won't break your persistence.
Using Model Classes in Controllers
Let's look at how the model classes are used in the controllers:
1. Creating New Objects
In AdminPanelController.onAddAlbumClicked():
Album newAlbum = new Album();
newAlbum.name = "New Album";
newAlbum.artist = "Unknown Artist";
newAlbum.genre = Genre.UNKNOWN;
Jukebox.database.albums.add(newAlbum);
albums.add(newAlbum);
2. Accessing Data
In AdminPanelController.setupSelectionListeners():
albumsList.getSelectionModel().selectedItemProperty().addListener(
(obs, oldSelection, newSelection) -> {
if (newSelection != null) {
// Access album fields directly
albumNameField.setText(newSelection.name);
albumArtistField.setText(newSelection.artist);
genreComboBox.getSelectionModel().select(newSelection.genre);
// Access album's songs list
songs.setAll(newSelection.songs);
}
}
);
3. Modifying Objects
In AdminPanelController.setupSelectionListeners() - auto-update:
albumNameField.textProperty().addListener((obs, oldVal, newVal) -> {
if (selectedAlbum != null) {
selectedAlbum.name = newVal; // Direct field modification
albumsList.refresh();
}
});
Creating Your Own Model Classes
Let's create a simple model hierarchy similar to Jukebox:
Step 1: Create an Enum
package myapp.model;
public enum Status {
ACTIVE,
INACTIVE,
PENDING
}
Step 2: Create a Simple Model Class
package myapp.model;
public class Task {
public String title = "";
public String description = "";
public Status status = Status.PENDING;
@Override
public String toString() {
return title + " [" + status + "]";
}
}
Step 3: Create a Container Model Class
package myapp.model;
import java.util.ArrayList;
import java.util.List;
public class Project {
public String name = "";
public List<Task> tasks = new ArrayList<>();
@Override
public String toString() {
return name + " (" + tasks.size() + " tasks)";
}
}
Step 4: Create the Database Class
package myapp.model;
import java.util.ArrayList;
import java.util.List;
public class AppDatabase {
public List<Project> projects = new ArrayList<>();
// Methods to add, remove, find projects
public void addProject(Project project) {
projects.add(project);
}
public void removeProject(Project project) {
projects.remove(project);
}
}
Try This:
- Open
Album.javaand examine the model class structure - Find the
toString()method and see how it formats the display - Open
Genre.javaand see how the enum is defined - Open
Database.javaand see how it contains the User and albums - Find where Database is created in
Jukebox.java - Find where Database is used in the controllers
Common Pitfalls
If you don't override toString() in your model classes, ListView and ComboBox will display the default Object.toString() which looks like jukebox.model.Album@1a2b3c4d - not very user-friendly!
If you're iterating through a list (like songs in an album) and modifying it at the same time, you'll get a ConcurrentModificationException. Use an Iterator or create a copy of the list first.
Always initialize your collection fields, otherwise you'll get NullPointerException when trying to add to them. In Jukebox, they use = new ArrayList<>() to initialize.
Be careful with circular references (e.g., Album references Song, and Song references Album). Gson can handle simple circular references, but complex ones might cause infinite loops during serialization.
Key Takeaways
- Model classes represent your application's data
- Keep model classes simple and independent of JavaFX
- Use composition to build complex models from simple ones
- Override
toString()for better display in UI controls - Use enums for fixed sets of values (like Genre)
- Initialize collections to avoid NullPointerException
- The Database class acts as a central data store
- Public fields are used in Jukebox for simplicity, but consider private fields with getters/setters for production