3
\$\begingroup\$

I'm new to Java and I've written a flashcard program in Java 8 that allows the user to create a collection of flashcards and then test themselves using those flashcards. Please provide any recommendations on the readability and the design of the program (including my use of OOP).

Here's some specific areas that might be helpful:

  1. Have I used encapsulation properly?

  2. Is my use of inner classes in QuizCardPlayer ok?

  3. In the build() method in QuizCardPlayer, would it be better to use SwingUtilities.invokeLater() inside every other build***() method instead of using it in just the build() method?

Any feedback regarding my JavaDoc style comments are welcome too (I felt that a lot of the functions are self-descriptive enough for there to not be a need for comments).

MainCode (this class only really exists to create an instance of the flashcard program)

/** MainCode - creates an instance of a simple flashcard program */
public class MainCode {
    public static void main(String[] args){
        MainCode q = new MainCode();
        q.go();
    }
    private void go(){
        QuizCardBuilder quizCardBuilder = new QuizCardBuilder(new Deck());
        quizCardBuilder.build();
    }
}

QuizCardBuilder (creates and edits a collection of flashcards)

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

/** QuizCardBuilder - This class allows the user to create, edit and save a Deck of QuizCards. */
public class QuizCardBuilder {
    private Deck deck;
    private JButton button;
    private JFileChooser fileChooser = new JFileChooser();
    private JFrame frame;
    private JTextArea answerText = new JTextArea();
    private JTextArea questionText = new JTextArea();
    private JPanel panel;

    private QuizCardPlayer quizCardPlayer;


    public QuizCardBuilder(Deck deck) {
        this.deck = deck;
        createQuizCardPlayer();
    }

    /** addCard - adds a QuizCard to the current Deck. */
    private void addCard(){
        deck.addQuizCard(getQuestionText().getText(), getAnswerText().getText());
        setQuestionText(null);
        setAnswerText(null);
    }

    void build() {
        SwingUtilities.invokeLater(
                () -> {
                        buildFrame();
                        buildContentPane();
                        buildMenuBar();
                        buildLabel(new JLabel("Question:"));
                        buildTextArea(questionText);
                        buildLabel(new JLabel("Answer:"));
                        buildTextArea(answerText);
                        buildButtonPanel();
                        displayFrame();
                        questionText.requestFocusInWindow();
                }
        );

    }

    private void buildButtonPanel() {
        button = new JButton("Add");
        button.setAlignmentX(Component.LEFT_ALIGNMENT);
        button.addActionListener(ev -> addCard());
        panel.add(button);
    }

    private void buildContentPane() {
        panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.setBorder(BorderFactory.createEmptyBorder(5, 15, 0, 15));
        frame.setContentPane(panel);
    }

    private void buildFrame() {
        frame = new JFrame("Quiz card builder - " + deck.getFileName());
        frame.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
        frame.setMinimumSize(new Dimension(400, 400));
        frame.addWindowListener(
                new WindowAdapter() {
                    @Override
                    public void windowClosing(WindowEvent e) {
                        close();
                    }
                }
        );
    }

    private void buildLabel(JLabel label) {
        label.setAlignmentX(Component.LEFT_ALIGNMENT);
        label.setFont(FontConstants.labelFont);
        panel.add(label);
    }

    private void buildMenuBar() {
        JMenuBar jMenuBar = new JMenuBar();
        JMenu file = new JMenu("File");
        file.add(Open);
        file.add(Save);
        file.add(SaveAs);
        file.add(Exit);

        JMenu card = new JMenu("Deck");
        card.add(ShuffleDeck);
        card.add(Play);

        jMenuBar.add(file);
        jMenuBar.add(card);
        frame.setJMenuBar(jMenuBar);
    }

    private void buildTextArea(JTextArea jTextArea) {
        jTextArea.setWrapStyleWord(true);
        jTextArea.setLineWrap(true);
        jTextArea.setFont(FontConstants.textAreaFont);
        jTextArea.addKeyListener(new KeyAdapter() {
            @Override
            public void keyTyped(KeyEvent e) {
                deck.setIsModified(true);
            }
        });
        JScrollPane jsp = new JScrollPane(jTextArea);
        jsp.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
        jsp.setAlignmentX(Component.LEFT_ALIGNMENT);
        panel.add(jsp);
    }

    private void close(){
        if (deck.getIsModified()) {
            // Automatically closes the program if there's nothing to be saved.
            if(deck.getQuizCardList().size() == 0 && getQuestionText().getText().length() == 0
                    && getAnswerText().getText().length() == 0) {
                System.exit(0);
            }else {
                int optionChosen = JOptionPane.showConfirmDialog(frame, "Do you want to save this deck?", "Save",
                        JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
                if (optionChosen == JOptionPane.YES_OPTION) {
                    save();
                }
                if (optionChosen != JOptionPane.CANCEL_OPTION) {
                    System.exit(0);
                }
            }
        }else{
            System.exit(0);
        }
    }

    /** createQuizCardPlayer - safely creates an instance of QuizCardPlayer, whilst allowing QuizCardPlayer to
     * have a callback */
    private void createQuizCardPlayer(){
        quizCardPlayer = new QuizCardPlayer(deck);
        quizCardPlayer.registerQuizCardBuilder(this);   // registers the callback
    }

    private void displayFrame() {
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    /** openFile - opens a saved Deck */
    private void openFile(){
        int optionChosen = JOptionPane.YES_OPTION;
        if(deck.getIsModified()){
            optionChosen = JOptionPane.showConfirmDialog(frame, "Do you want to save this deck before " +
                            "opening another?", "Save", JOptionPane.YES_NO_CANCEL_OPTION,JOptionPane.QUESTION_MESSAGE);
            if(optionChosen == JOptionPane.YES_OPTION){
                save();
            }
        }

        if(optionChosen != JOptionPane.CANCEL_OPTION && fileChooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION){
            deck = new Deck();
            deck.readFile(fileChooser.getSelectedFile().getAbsolutePath());
            setTitle(deck.getFileName());
            setQuestionText(null);
            setAnswerText(null);
        }
    }

    /** save - Saves the current Deck under the same name, if previously saved. If the Deck is new,
     * then saveAs is invoked */
    private void save(){
        if(deck.getFileName().equals("Untitled")){
            saveAs();
        }else{
            if(getQuestionText().getText().length() > 0){
                addCard();
            }
            deck.save(deck.getFileLocation());
            deck.setIsModified(false);
        }
    }

    /** saveAs - User gets to choose the filename that stores the current Deck */
    private void saveAs(){
        if(fileChooser.showSaveDialog(frame) == JFileChooser.APPROVE_OPTION) {
            if(getQuestionText().getText().length() > 0){
                addCard();
            }
            deck.save(fileChooser.getSelectedFile().getAbsolutePath());
            deck.setFileName(fileChooser.getSelectedFile().getName());
            setTitle(deck.getFileName());
            deck.setIsModified(false);
        }
    }


    // GETTERS
    private JTextArea getAnswerText() {
        return answerText;
    }

    JTextArea getQuestionText() {
        return questionText;
    }


    // SETTERS
    private void setAnswerText(String text) {
        SwingUtilities.invokeLater(() -> answerText.setText(text));
    }

    void setTextAreaEditability(boolean isEditable){
        questionText.setEditable(isEditable);
        answerText.setEditable(isEditable);
        button.setEnabled(isEditable);
    }

    private void setTitle(String newTitle){
        SwingUtilities.invokeLater(() -> frame.setTitle("Quiz Card Builder - " + newTitle));
    }

    private void setQuestionText(String text) {
        SwingUtilities.invokeLater(() -> questionText.setText(text));
    }




    // ACTIONS
    private Action Exit = new AbstractAction("Quit"){
        @Override
        public void actionPerformed(ActionEvent ev){
            close();
        }
    };

    private Action Open = new AbstractAction("Open"){
        @Override
        public void actionPerformed(ActionEvent ev){
            openFile();
        }
    };

    private Action Play = new AbstractAction("Begin test"){
        @Override
        public void actionPerformed(ActionEvent ev){
            // Allows the user to open a file if no file is already open
            if(deck.getQuizCardList().size() == 0) {
                openFile();
            }

            // Prevents window from popping up if there's no QuizCards to use
            if(deck.getQuizCardList().size() > 0) {
                if (deck.getIsTestRunning()) {
                    Toolkit.getDefaultToolkit().beep();
                    quizCardPlayer.toFront();
                } else {
                    deck.setIsTestRunning(true);
                    setTextAreaEditability(false);
                    createQuizCardPlayer();
                    quizCardPlayer.build();
                }
            }
        }
    };

    private Action Save = new AbstractAction("Save"){
        @Override
        public void actionPerformed(ActionEvent ev){
            save();
        }
    };

    private Action SaveAs = new AbstractAction("Save as...") {
        @Override
        public void actionPerformed(ActionEvent e) {
            saveAs();
        }
    };

    private Action ShuffleDeck = new AbstractAction("Shuffle deck"){
        @Override
        public void actionPerformed(ActionEvent ev){
            deck.shuffle();
        }
    };
}

QuizCardPlayer (this class allows the user to test themselves)

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.*;

/** QuizCardPlayer - This class allows the user to test themselves using a specific Deck of QuizCards. */
public class QuizCardPlayer {
    private static final Dimension FRAME_SIZE = new Dimension(300, 300);
    private static final Dimension MINIMUM_FRAME_SIZE = new Dimension(200, 200);

    private int deckIndex;
    private boolean isAnswerShown;
    private Deck deck;
    private JButton correctButton, showAnswerButton, wrongButton;
    private JFrame frame;
    private JLabel label;
    private JPanel contentPane;
    private JTextArea textArea;

    private QuizCardBuilder quizCardBuilder;


    public QuizCardPlayer(Deck deck){
        this.deck = deck;
    }

    void build(){
        SwingUtilities.invokeLater(
                () -> {
                    buildFrame();
                    buildContentPane();
                    buildLabel();
                    buildTextArea();
                    buildButtonPanel();
                    displayFrame();
                    showAnswerButton.requestFocusInWindow();
                }
        );
    }

    private void buildButtonPanel(){
            showAnswerButton = new JButton("Show answer");
            showAnswerButton.addActionListener(new ButtonListener());
            correctButton = new JButton("Right");
            correctButton.addActionListener(new CorrectButtonListener());
            correctButton.setVisible(false);
            wrongButton = new JButton("Wrong");
            wrongButton.addActionListener(new WrongButtonListener());
            wrongButton.setVisible(false);

            JPanel buttonPanel = new JPanel();
            buttonPanel.add(showAnswerButton);
            buttonPanel.add(correctButton);
            buttonPanel.add(wrongButton);
            buttonPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
            contentPane.add(BorderLayout.SOUTH, buttonPanel);
    }

    private void buildContentPane(){
        contentPane = new JPanel();
        contentPane.setLayout(new BorderLayout());
        contentPane.setBorder(BorderFactory.createEmptyBorder(5, 15, 0, 15));
        frame.setContentPane(contentPane);
    }

    private void buildFrame(){
        frame = new JFrame("Quiz card Player - " + deck.getFileName());
        frame.setMinimumSize(MINIMUM_FRAME_SIZE);
        frame.addWindowListener(
                new WindowAdapter() {
                    @Override
                    public void windowClosing(WindowEvent e) {
                        closeFrame();
                    }
                }
        );
    }

    private void buildLabel(){
        label = new JLabel("Question:");
        label.setFont(FontConstants.labelFont);
        label.setAlignmentX(Component.LEFT_ALIGNMENT);
        contentPane.add(BorderLayout.NORTH, label);
    }

    private void buildTextArea(){
        textArea = new JTextArea();
        textArea.setEditable(false);
        textArea.setLineWrap(true);
        textArea.setWrapStyleWord(true);
        textArea.setText(deck.getQuizCardList().get(0).getQuestion());
        textArea.setFont(FontConstants.textAreaFont);
        JScrollPane jsp = new JScrollPane(textArea);
        jsp.setAlignmentX(Component.LEFT_ALIGNMENT);
        contentPane.add(BorderLayout.CENTER, jsp);

    }

    private void closeFrame(){
        SwingUtilities.invokeLater(frame::dispose);
        deck.setIsTestRunning(false);
        deck.setNumCorrect(0);
        deck.setNumWrong(0);
        quizCardBuilder.setTextAreaEditability(true);
        quizCardBuilder.getQuestionText().requestFocusInWindow();
    }

    private void displayFrame(){
        frame.setSize(FRAME_SIZE);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    /** registerQuizCardBuilder - A callback function that allows an instance of QuizCardPlayer to pass info back to
     * the specified instance of QuizCardBuilder. */
    void registerQuizCardBuilder(QuizCardBuilder newQuizCardBuilder){
        quizCardBuilder = newQuizCardBuilder;
    }

    /** toFront - brings this frame in the JVM to the front. */
    void toFront(){
        SwingUtilities.invokeLater(frame::toFront);
    }


    // LISTENERS
    private class CorrectButtonListener extends ButtonListener {
        @Override
        public void actionPerformed(ActionEvent ev){
            deck.setNumCorrect(deck.getNumCorrect() + 1);
            super.actionPerformed(ev);
        }
    }

    private class WrongButtonListener extends ButtonListener {
        // TODO why is it possible to have a public method in a private class?
        @Override
        public void actionPerformed(ActionEvent ev){
            deck.setNumWrong(deck.getNumWrong() + 1);
            super.actionPerformed(ev);
        }
    }

    private class ButtonListener implements ActionListener {
        @Override
        public void actionPerformed(ActionEvent ev){
            if(deckIndex < deck.getQuizCardList().size()) {
                if (isAnswerShown) {
                    showNextCard();
                } else {
                    showAnswer();
                }
            }else if(deckIndex == deck.getQuizCardList().size()) {
                showResults();
            }else{
                closeFrame();
            }
        }

        private void showAnswer(){
            SwingUtilities.invokeLater(
                    () -> {
                        label.setText("Answer:");
                        textArea.setText(deck.getQuizCardList().get(deckIndex).getAnswer());
                        isAnswerShown = true;
                        showAnswerButton.setVisible(false);
                        correctButton.setVisible(true);
                        correctButton.requestFocusInWindow();
                        wrongButton.setVisible(true);

                        deckIndex++;
                    }
            );
        }

        private void showNextCard(){
            SwingUtilities.invokeLater(
                    () -> {
                        label.setText("Question:");
                        textArea.setText(deck.getQuizCardList().get(deckIndex).getQuestion());
                        isAnswerShown = false;
                        showAnswerButton.setText("Show answer");
                        showAnswerButton.setVisible(true);
                        showAnswerButton.requestFocusInWindow();
                        correctButton.setVisible(false);
                        wrongButton.setVisible(false);
                    }
            );
        }

        private void showResults(){
            SwingUtilities.invokeLater(
                    () -> {
                        label.setText("Results:");
                        textArea.setText("You got " + deck.getNumCorrect() + " correct and " + deck.getNumWrong() + " wrong.");
                        showAnswerButton.setText("End");
                        showAnswerButton.setVisible(true);
                        showAnswerButton.requestFocusInWindow();
                        correctButton.setVisible(false);
                        wrongButton.setVisible(false);
                        deckIndex++;
                    }
            );
        }
    }
}

Deck (manages the collection of flashcards)

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/** Deck - This class deals with a collection of QuizCards */
public class Deck {
    private File file;
    private List<QuizCard> quizCardList = new ArrayList<>();
    private String fileName = "Untitled";
    private boolean isModified;
    private boolean isTestRunning;
    private int numCorrect;
    private int numWrong;

    private static final String QUIZ_CARD_TERMINATOR = "\n29rje2r9\n";
    private static final String QUIZ_CARD_SEPARATOR = "\te23bf0hj\t";

    /** addQuizCard - creates and adds a QuizCard to quizCardList */
    void addQuizCard(String q, String a){
        // Prevents any parsing exceptions occurring when opening a file
        if(q.length() == 0){
            q = " ";
        }
        if(a.length() == 0){
            a = " ";
        }
        quizCardList.add(new QuizCard(q, a));
    }

    /** parseData - parses the data from an input String using specified terminators and separators. */
    private void parseData(String unparsedData) {
        String[] stageOne = unparsedData.split(QUIZ_CARD_TERMINATOR);

        for (String stageTwo : stageOne) {
            String[] quizCardData = stageTwo.split(QUIZ_CARD_SEPARATOR);
            addQuizCard(quizCardData[0], quizCardData[1]);
        }
    }

    /** readFile - loads in the data from a saved deck into quizCardList */
    void readFile(String fileLocation){
        file = new File(fileLocation);
        setFileName(file.getName());
        assert file.canRead();
        try(BufferedReader input = new BufferedReader(new FileReader(file))){
            int letterNumber;
            StringBuilder dataToParse = new StringBuilder();
            while((letterNumber = input.read()) != -1){
                dataToParse.append((char) letterNumber);
            }
            parseData(dataToParse.toString());
        }catch(IOException ioEx){
            ioEx.printStackTrace();
        }
    }

    /** save - saves the Deck to specified file location */
    void save(String fileLocation){
        file = new File(fileLocation);
        assert file.canWrite();
        try (BufferedWriter output = new BufferedWriter(new FileWriter(file))) {
            for(QuizCard quizCard : quizCardList){
                output.write(quizCard.getQuestion() + QUIZ_CARD_SEPARATOR + quizCard.getAnswer() + QUIZ_CARD_TERMINATOR);
            }
        }catch(IOException ioEx){
            ioEx.printStackTrace();
        }
    }

    /** shuffle - Shuffles the deck in place. If saved, the quiz cards will be saved in the new shuffled order. */
    void shuffle(){
        Collections.shuffle(quizCardList);
    }


    // GETTERS
    String getFileLocation(){
        return file.getAbsolutePath();
    }

    String getFileName(){
        return fileName;
    }

    boolean getIsModified(){
        return isModified;
    }

    boolean getIsTestRunning(){
        return isTestRunning;
    }

    int getNumCorrect(){
        return numCorrect;
    }

    int getNumWrong(){
        return numWrong;
    }

    List<QuizCard> getQuizCardList(){
        return quizCardList;
    }


    // SETTERS
    void setFileName(String fileName) {
        if(fileName.contains(".")){
            fileName = fileName.split("\\.")[0];
        }
        this.fileName = fileName;
    }

    void setIsModified(boolean newValue){
        isModified = newValue;
    }

    void setIsTestRunning(boolean newValue){
        isTestRunning = newValue;
    }

    void setNumCorrect(int newValue){
        numCorrect = newValue;
    }

    void setNumWrong(int newValue){
        numWrong = newValue;
    }

}

QuizCard (the class for one flashcard)

/** QuizCard - Class for one flash card */
public class QuizCard {
    private String question;
    private String answer;

    public QuizCard(String f, String b){
        setQuestion(f);
        setAnswer(b);
    }

    // GETTERS
    String getAnswer(){
        return answer;
    }

    String getQuestion(){
        return question;
    }


    // SETTERS
    void setAnswer(String text){
        answer = text;
    }

    void setQuestion(String text){
        question = text;
    }
}

FontConstants (holds the fonts used throughout the program)

import java.awt.Font;
import javax.swing.UIManager;

/** FontConstants - Class used to set the fonts */
public class FontConstants {
    public static final Font labelFont = new Font(UIManager.getDefaults().getFont("TabbedPane.font").getFamily(),
            Font.PLAIN, 14);
    public static final Font textAreaFont = new Font(UIManager.getDefaults().getFont("TabbedPane.font").getFamily(),
            Font.PLAIN, 16);
}
\$\endgroup\$
1
\$\begingroup\$

I'm going to answer the following question:

Is my use of inner classes in QuizCardPlayer ok?

First you can use import static to reduce clutter caused by utility methods being used many times.

import static javax.swing.SwingUtilities.invokeLater;

Then decrease level of indentation by pulling invokeLater a level up.

//to be used as `invokeLater(this::showAnswer)` etc.
private void showAnswer() {
    // just the body of the invokeLater
}

private void showNextCard() {
    // just the body of the invokeLater
}

private void showResults(){
    // just the body of the invokeLater
}

private void closeFrame(){
    // invokeLater removed
    frame.dispose();
    //.... some code ignored
}

You can then disentangle the spaghetti in ButtonListener.actionPerformed.

private Runnable getAction() {
    if (deckIndex > deck.getQuizCardList().size())
        return QuizCardPlayer.this::closeFrame;

    if (deckIndex == deck.getQuizCardList().size())
        return this::showResults;

    return isAnswerShown ? this::showNextCard : this::showAnswer;
}

Then remove the inner classes altogether, and use lambdas instead.

private void buildButtonPanel(){
    //.... some code ignored

    showAnswerButton.addActionListener(ev -> invokeLater(getAction()));

    correctButton.addActionListener(ev -> {
        deck.setNumCorrect(deck.getNumCorrect() + 1);
        invokeLater(getAction());
    });

    wrongButton.addActionListener(ev -> {
        deck.setNumWrong(deck.getNumWrong() + 1);
        invokeLater(getAction());
    });
}

It is now clear that most of the spaghetti in ButtonListener.actionPerformed is unnecessary:

private void showNextCardOrResults() {
    deckIndex < deck.getQuizCardList().size()
        ? this.showNextCard()
        : this.showResults();
}


private void buildButtonPanel(){
    //.... some code ignored

    showAnswerButton.addActionListener(ev -> invokeLater(this::showAnswer));

    correctButton.addActionListener(ev -> {
        deck.setNumCorrect(deck.getNumCorrect() + 1);
        invokeLater(this::showNextCardOrResults);
    });

    wrongButton.addActionListener(ev -> {
        deck.setNumWrong(deck.getNumWrong() + 1);
        invokeLater(this::showNextCardOrResults);
    });
}
\$\endgroup\$
  • \$\begingroup\$ Great thanks for this! I’m still getting to grips with Java 8 and I’m a little confused on a couple of things. Firstly, why can a method reference (or more generally, lambda expression) be returned in a method with Runnable as its return type? I understand that using this::showAnswer returns a Runnable object whereas this.showAnswer() does not. Also,showAnswer(), showNextCard() and showResults() have a void return type so I’m a bit confused as to why one can return these methods in this manner? \$\endgroup\$ – Calculus5000 Mar 18 '15 at 16:13
  • 1
    \$\begingroup\$ Runnable is a functional interface with a single abstract method. I can return method references with matching signatures as a functional interface. e.g. reference to this.showResults():void as Runnable.run():void. \$\endgroup\$ – abuzittin gillifirca Mar 19 '15 at 11:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.