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.

What You'll Learn:
  • 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;
    }
}
Album Class Features:
  • 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;
    }
}
Song Class Features:
  • 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
}
Why Use Enums?
  • 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 = "";
}
User Class:

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() { ... }
}
Database Class Features:
  • 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           │
└─────────────────┘
Relationship Summary:
  • Database has-a User and a List of Albums
  • Album has-a List of Songs
  • Song is a simple data class with no child objects
  • User is a simple data class with no child objects
  • Genre is 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:

Why toString() Matters:
  • 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; }
}
Note about Gson:

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:

    1. Open Album.java and examine the model class structure
    2. Find the toString() method and see how it formats the display
    3. Open Genre.java and see how the enum is defined
    4. Open Database.java and see how it contains the User and albums
    5. Find where Database is created in Jukebox.java
    6. Find where Database is used in the controllers

    Common Pitfalls

    Pitfall 1: Not Implementing toString()

    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!

    Pitfall 2: Modifying Collections While Iterating

    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.

    Pitfall 3: Not Initializing Collections

    Always initialize your collection fields, otherwise you'll get NullPointerException when trying to add to them. In Jukebox, they use = new ArrayList<>() to initialize.

    Pitfall 4: Circular References

    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

    Related Cookbook Sections