One of the best ways to learn Java is to build something small enough to understand but complex enough to force you to solve real programming problems. Tic-tac-toe is ideal because it combines arrays, loops, methods, input validation, conditional logic and state management.

This version modernizes the example for Java 25. It uses a compact source file and Java 25's IO.readln and IO.println methods instead of repeatedly constructing a Scanner or calling System.out.

How the Java 25 tic-tac-toe game works

The game follows a straightforward loop:

  1. Create a nine-element board.
  2. Print the board.
  3. Ask the current player for a square.
  4. Validate the input.
  5. Place an X or O on the board.
  6. Check the eight possible winning lines.
  7. Declare a winner or a draw.
  8. Switch players and continue.

Step 1: Create the board

A one-dimensional array is sufficient for a 3-by-3 board. Each unused square initially contains the digit the player enters to select it:

var board = new char[] {
    '1', '2', '3',
    '4', '5', '6',
    '7', '8', '9'
};

A two-dimensional array would also work, but the one-dimensional representation keeps indexing and win detection simple.

Step 2: Print the board with Java 25 IO

Java 25's IO class makes console examples concise. The board-printing method can use text blocks to keep the layout readable:

void printBoard(char[] board) {
    IO.println("""
         %s | %s | %s
        ---+---+---
         %s | %s | %s
        ---+---+---
         %s | %s | %s
        """.formatted(
            board[0], board[1], board[2],
            board[3], board[4], board[5],
            board[6], board[7], board[8]
        ));
}

The initial board appears as:

 1 | 2 | 3
---+---+---
 4 | 5 | 6
---+---+---
 7 | 8 | 9

Step 3: Read and validate player input

The older version created Scanner instances inside the game loop. Java 25 lets a compact console application use IO.readln directly.

The following method keeps asking until the player enters an integer from 1 through 9 that identifies an unused square:

int readMove(char[] board, char player) {
    while (true) {
        var input = IO.readln(
            "Player " + player + ", choose a square (1-9): "
        );

        try {
            var square = Integer.parseInt(input);

            if (square < 1 || square > 9) {
                IO.println("Choose a number from 1 through 9.");
                continue;
            }

            var index = square - 1;

            if (!Character.isDigit(board[index])) {
                IO.println("That square is already taken.");
                continue;
            }

            return index;
        } catch (NumberFormatException e) {
            IO.println("Enter a whole number from 1 through 9.");
        }
    }
}

This avoids a common bug in beginner implementations where invalid input causes an array-index error or where a player can overwrite an occupied square.

Step 4: Check for a winner

Tic-tac-toe has eight possible winning lines. Rather than relying on arithmetic with char values, represent those lines explicitly. This approach is easier to read, explain and extend.

final int[][] WINNING_LINES = {
    {0, 1, 2},
    {3, 4, 5},
    {6, 7, 8},
    {0, 3, 6},
    {1, 4, 7},
    {2, 5, 8},
    {0, 4, 8},
    {2, 4, 6}
};

boolean hasWon(char[] board, char player) {
    for (var line : WINNING_LINES) {
        if (board[line[0]] == player
                && board[line[1]] == player
                && board[line[2]] == player) {
            return true;
        }
    }

    return false;
}

The older trick of adding three char values technically works because char is an integral type, but explicit winning combinations better communicate the rules of the game.

Step 5: Switch players

After a valid move that does not win the game, switch between X and O:

player = player == 'X' ? 'O' : 'X';

The conditional operator is compact and appropriate here because both alternatives are simple values.

Step 6: Detect a draw

A game is a draw when all nine valid moves have been made and neither player has won. Keeping a move counter makes this check simple:

if (moves == board.length) {
    printBoard(board);
    IO.println("The game is a draw.");
    return;
}

Complete Java 25 tic-tac-toe source code

Java 25 compact source files let this beginner example focus on the game instead of class boilerplate. Save the following as TicTacToe.java:

final int[][] WINNING_LINES = {
    {0, 1, 2},
    {3, 4, 5},
    {6, 7, 8},
    {0, 3, 6},
    {1, 4, 7},
    {2, 5, 8},
    {0, 4, 8},
    {2, 4, 6}
};

void main() {
    var board = new char[] {
        '1', '2', '3',
        '4', '5', '6',
        '7', '8', '9'
    };

    var player = 'X';
    var moves = 0;

    IO.println("Java 25 Tic-Tac-Toe");

    while (true) {
        printBoard(board);

        var index = readMove(board, player);
        board[index] = player;
        moves++;

        if (hasWon(board, player)) {
            printBoard(board);
            IO.println("Player " + player + " wins!");
            return;
        }

        if (moves == board.length) {
            printBoard(board);
            IO.println("The game is a draw.");
            return;
        }

        player = player == 'X' ? 'O' : 'X';
    }
}

int readMove(char[] board, char player) {
    while (true) {
        var input = IO.readln(
            "Player " + player + ", choose a square (1-9): "
        );

        try {
            var square = Integer.parseInt(input);

            if (square < 1 || square > 9) {
                IO.println("Choose a number from 1 through 9.");
                continue;
            }

            var index = square - 1;

            if (!Character.isDigit(board[index])) {
                IO.println("That square is already taken.");
                continue;
            }

            return index;
        } catch (NumberFormatException e) {
            IO.println("Enter a whole number from 1 through 9.");
        }
    }
}

boolean hasWon(char[] board, char player) {
    for (var line : WINNING_LINES) {
        if (board[line[0]] == player
                && board[line[1]] == player
                && board[line[2]] == player) {
            return true;
        }
    }

    return false;
}

void printBoard(char[] board) {
    IO.println("""
         %s | %s | %s
        ---+---+---
         %s | %s | %s
        ---+---+---
         %s | %s | %s
        """.formatted(
            board[0], board[1], board[2],
            board[3], board[4], board[5],
            board[6], board[7], board[8]
        ));
}

Run the Java 25 game

Compile and run the source file with a Java 25 JDK:

javac TicTacToe.java
java TicTacToe

Or use Java's source-file launcher:

java TicTacToe.java

Ideas to improve the game

  • Let players choose their names.
  • Add a replay option.
  • Track wins across multiple games.
  • Use an enum for the players.
  • Create an unbeatable computer opponent.
  • Separate the board, game rules and user interface into different types.
  • Add unit tests for every winning combination.
  • Build a graphical version with JavaFX.

The result is still intentionally a beginner-friendly program, but it now uses modern Java 25 syntax and APIs, validates user input correctly, avoids repeatedly creating input scanners and expresses the winning logic much more clearly.


Cameron McKenzie

Cameron McKenzie is an AWS Certified AI Practitioner, Machine Learning Engineer, Solutions Architect and author of many popular books in the software development and Cloud Computing space. His growing YouTube channel training devs in Java, Spring, AI and ML has well over 30,000 subscribers.