Firstly, I would use some kind of model to control the values within a "virtual" board, this will separate the logic from the UI and allow either to change without adversely effecting the other.
I would provide appropriate event with the model to allow the UI to be updated when the model changes as well as providing a means for each field to update the model as required.
I would then reduce the problem to it's smallest conceptional component, which would be the sub board, and generate the UI to represent it in the most abstract manner I could. This allows for re-use and helps with debugging, as if one board has an issue, then you can fix it in one place for all.
public class Sudoku {
public static void main(String[] args) {
new Sudoku();
}
public Sudoku() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException ex) {
} catch (InstantiationException ex) {
} catch (IllegalAccessException ex) {
} catch (UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new SudokuBoard());
frame.add(new MenuPane(), BorderLayout.AFTER_LINE_ENDS);
frame.pack();
frame.setVisible(true);
}
});
}
public class MenuPane extends JPanel {
public MenuPane() {
setBorder(new EmptyBorder(4, 4, 4, 4));
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 1;
gbc.weightx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
add(new JButton("Solve"), gbc);
gbc.gridy++;
add(new JButton("New"), gbc);
gbc.gridy++;
add(new JButton("Hint"), gbc);
gbc.gridy++;
add(new JButton("Reset"), gbc);
}
}
public class SudokuBoard extends JPanel {
public static final int ROWS = 3;
public static final int COLUMNS = 3;
private SubBoard[] subBoards;
public SudokuBoard() {
setBorder(new EmptyBorder(4, 4, 4, 4));
subBoards = new SubBoard[ROWS * COLUMNS];
setLayout(new GridLayout(ROWS, COLUMNS, 2, 2));
for (int row = 0; row < ROWS; row++) {
for (int col = 0; col < COLUMNS; col++) {
int index = (row * ROWS) + col;
SubBoard board = new SubBoard();
board.setBorder(new CompoundBorder(new LineBorder(Color.GRAY, 3), new EmptyBorder(4, 4, 4, 4)));
subBoards[index] = board;
add(board);
}
}
}
}
public class SubBoard extends JPanel {
public static final int ROWS = 9;
public static final int COLUMNS = 9;
private JTextField[] fields;
public SubBoard() {
setLayout(new GridLayout(ROWS, COLUMNS, 2, 2));
fields = new JTextField[ROWS * COLUMNS];
for (int row = 0; row < ROWS; row++) {
for (int col = 0; col < COLUMNS; col++) {
int index = (row * COLUMNS) + col;
JTextField field = new JTextField(4);
fields[index] = field;
// field.setText(Integer.toString(index));
add(field);
}
}
}
}
}
UPDATED
To limit the text fields to only allow numeric values to be entered, you can take a look at JTextField limiting character amount input and accepting numeric only for some ideas

UPDATED (with 2D arrays)
Here's a implementation that uses 2D arrays, it also sub groups the sub boards so that each grid of 3x3 fields has it's own board...

public class Sudoku {
public static void main(String[] args) {
new Sudoku();
}
public Sudoku() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException ex) {
} catch (InstantiationException ex) {
} catch (IllegalAccessException ex) {
} catch (UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new SudokuBoard());
frame.add(new MenuPane(), BorderLayout.AFTER_LINE_ENDS);
frame.pack();
frame.setVisible(true);
}
});
}
public class MenuPane extends JPanel {
public MenuPane() {
setBorder(new EmptyBorder(4, 4, 4, 4));
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 1;
gbc.weightx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
add(new JButton("Solve"), gbc);
gbc.gridy++;
add(new JButton("New"), gbc);
gbc.gridy++;
add(new JButton("Hint"), gbc);
gbc.gridy++;
add(new JButton("Reset"), gbc);
}
}
public class SudokuBoard extends JPanel {
public static final int ROWS = 3;
public static final int COLUMNS = 3;
private SubBoard[][] subBoards;
public SudokuBoard() {
setBorder(new EmptyBorder(4, 4, 4, 4));
subBoards = new SubBoard[ROWS][COLUMNS];
setLayout(new GridLayout(ROWS, COLUMNS, 2, 2));
for (int row = 0; row < ROWS; row++) {
for (int col = 0; col < COLUMNS; col++) {
int index = (row * ROWS) + col;
SubBoard board = new SubBoard();
board.setBorder(new CompoundBorder(new LineBorder(Color.GRAY, 3), new EmptyBorder(4, 4, 4, 4)));
subBoards[row][col] = board;
add(board);
}
}
}
}
public class SubBoard extends JPanel {
public SubBoard() {
setLayout(new GridLayout(3, 3, 2, 2));
for (int index = 0; index < 3*3; index++) {
add(new ChildBoard(3, 3));
}
}
}
public class ChildBoard extends JPanel {
private JTextField[][] fields;
public ChildBoard(int rows, int cols) {
setBorder(new LineBorder(Color.LIGHT_GRAY));
setLayout(new GridLayout(rows, cols, 2, 2));
fields = new JTextField[rows][cols];
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
JTextField field = new JTextField(4);
fields[row][col] = field;
add(field);
}
}
}
}
}
or, if you want to try and keep all the fields in a single top level reference you could do something like...
public class SubBoard extends JPanel {
private JTextField[][] fields;
public SubBoard() {
setLayout(new GridLayout(3, 3, 2, 2));
fields = new JTextField[9][9];
for (int row = 0; row < 9; row++) {
for (int col = 0; col < 9; col++) {
fields[row][col] = new JTextField(4);
}
}
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 3; col++) {
int startRow = row * 3;
int startCol = col * 3;
add(new ChildBoard(3, 3, fields, startRow, startCol));
}
}
}
}
public class ChildBoard extends JPanel {
public ChildBoard(int rows, int cols, JTextField[][] fields, int startRow, int startCol) {
setBorder(new LineBorder(Color.LIGHT_GRAY));
setLayout(new GridLayout(rows, cols, 2, 2));
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
JTextField field = fields[startRow + row][startCol + col];
fields[row][col] = field;
add(field);
}
}
}
}
UPDATED with single class
Okay, so rather then sub-classing, simple use a couple of methods to create each individual section of the board, from which you can make repeated calls to...

Remember, reduce and reuse.
public class Sudoku {
public static void main(String[] args) {
new Sudoku();
}
public Sudoku() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException ex) {
} catch (InstantiationException ex) {
} catch (IllegalAccessException ex) {
} catch (UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new SudokuBoard());
frame.add(new MenuPane(), BorderLayout.AFTER_LINE_ENDS);
frame.pack();
frame.setVisible(true);
}
});
}
public class MenuPane extends JPanel {
public MenuPane() {
setBorder(new EmptyBorder(4, 4, 4, 4));
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 1;
gbc.weightx = 1;
gbc.fill = GridBagConstraints.HORIZONTAL;
add(new JButton("Solve"), gbc);
gbc.gridy++;
add(new JButton("New"), gbc);
gbc.gridy++;
add(new JButton("Hint"), gbc);
gbc.gridy++;
add(new JButton("Reset"), gbc);
}
}
public class SudokuBoard extends JPanel {
public static final int GRID_ROWS = 3;
public static final int GRID_COLUMNS = 3;
public static final int BOARD_ROWS = 9;
public static final int BOARD_COLUMNS = 9;
private JTextField fields[][];
public SudokuBoard() {
setBorder(new EmptyBorder(4, 4, 4, 4));
fields = new JTextField[GRID_ROWS * BOARD_ROWS][GRID_COLUMNS * BOARD_COLUMNS];
setLayout(new GridLayout(GRID_ROWS, GRID_COLUMNS, 2, 2));
for (int row = 0; row < GRID_ROWS; row++) {
for (int col = 0; col < GRID_COLUMNS; col++) {
int startRow = row * GRID_ROWS;
int startCol = col * GRID_COLUMNS;
add(createBoard(fields, startRow, startCol));
}
}
}
protected JPanel createBoard(JTextField fiels[][], int startRow, int startCol) {
JPanel panel = new JPanel(new GridLayout(3, 3, 2, 2));
panel.setBorder(new CompoundBorder(new LineBorder(Color.DARK_GRAY, 2), new EmptyBorder(2, 2, 2, 2)));
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 3; col++) {
int rowIndex = (startRow + row) * 3;
int colIndex = (startCol + col) * 3;
panel.add(createSubBoard(fields, rowIndex, colIndex));
}
}
return panel;
}
protected JPanel createSubBoard(JTextField[][] fields, int startRow, int startCol) {
JPanel panel = new JPanel(new GridLayout(3, 3, 2, 2));
panel.setBorder(new CompoundBorder(new LineBorder(Color.GRAY, 2), new EmptyBorder(2, 2, 2, 2)));
populateFields(fields, startRow, startCol);
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 3; col++) {
panel.add(fields[row + startRow][col + startCol]);
}
}
return panel;
}
protected void populateFields(JTextField[][] fields, int startRow, int startCol) {
for (int row = startRow; row < startRow + 3; row++) {
for (int col = startCol; col < startCol + 3; col++) {
fields[row][col] = new JTextField(4);
}
}
}
}
}