Almost all GUI applications have same behavior when they are closing. Window is closed when the user presses the "X" button or by some other means (menu item, ALT+F4). If the window contains some unsaved data it warns the user about it and allows him to cancel the closing. If the user chooses to close it, the window is removed from the screen and, optionally, the entire application is terminated.

To do all that in a Swing application you will need to take control over the window events.

In Swing a window is called frame and from now on I will use both names to refer to a window.

First you need to turn off the default close operation using the setDefaultCloseOperation(..) method.

public class ClosableFrame extends JFrame {
    
    private void initFrame() {
        this.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
    }
    
    public ClosableFrame() {
        super("Closable frame demo");
        this.setPreferredSize(new Dimension(400, 400));
        this.initFrame();
    }
    
}

Then you need to implement the WindowListener interface, or more elegantly, create new WindowAdapter object and override its windowClosing method.

public class ClosableFrame extends JFrame {
    
    private WindowAdapter windowAdapter = new WindowAdapter() {

        @Override
        public void windowClosing(WindowEvent e) {
            super.windowClosing(e);
            
            if ( !isDataSaved() ) {
                int answer = JOptionPane.showConfirmDialog(ClosableFrame.this,"Window contains unsaved data, do you really want to close?" , "Close?", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
                if ( answer == 1 ) {
                    // do not continue if user presses "No"
                    return;
                }
            }
            // close the frame, Java VM may terminate the application
            ClosableFrame.this.dispose();
        }
        
    };
    
    private void initFrame() {
        this.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        this.addWindowListener(this.windowAdapter); // DON'T FORGET THIS
    }
    
    public ClosableFrame() {
        super("Closable frame demo");
        this.setPreferredSize(new Dimension(400, 400));
        this.initFrame();
    }
    
    private boolean isDataSaved() {
        // this should contain some real logic
        return false;
    }
    
}

Don't forget to register your WindowAdapter object to listen for the window events of the frame.

This method is called when the WINDOW_CLOSING event is intercepted. This event is posted to the event queue when the user presses the "X"  button. You can programmatically force a frame into thinking that the "X" button is pressed by sending it the WINDOW_CLOSING event:

WindowEvent closingEvent = new WindowEvent(targetFrame, WindowEvent.WINDOW_CLOSING);
Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(closingEvent);

You can use this code to programmatically close a Swing frame when, for example, a button or menu item is pressed, on request from external component, etc...

I used the dispose() method in the windowClosing event handler to close the frame. This method destroys all components of a frame. If you want to show the frame again, you need to repack it which can be costly. In order to avoid this you can use the setVisible(false) method, which does not destroy components, it rather hides the frame. You can then show it again by calling the setVisible(true) method, you just need to store the reference to the frame somewhere in your application.

One notable characteristic of the dispose() method is that, if there are no other opened frames or active background tasks, it can trigger termination of the entire application.

If the frame controls application lifetime you will want to terminate the application when the frame is closed. You can not rely on the dispose() method because it does not guarantee termination, instead you must use the System.exit(..) method, and the best time to do it is after the frame is closed. In order to do this you will need to override the windowClosed(..) method of the WindowAdapter object:

public class ClosableFrame extends JFrame {
    
    private WindowAdapter windowAdapter = new WindowAdapter() {

        @Override
        public void windowClosing(WindowEvent e) {
            super.windowClosing(e);
            
            if ( !isDataSaved() ) {
                int answer = JOptionPane.showConfirmDialog(ClosableFrame.this,"Window contains unsaved data, do you really want to close?" , "Close?", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
                if ( answer == 1 ) {
                    // do not continue if user presses "No"
                    return;
                }
            }
            // close the frame, Java VM may terminate the application
            ClosableFrame.this.dispose();
        }

        @Override
        public void windowClosed(WindowEvent e) {
            super.windowClosed(e);
            
            // terminate the application if frame controls applications lifetime
            // this.getApplication().releaseAllResources();
            // this.getApplication().exit() <-- this should be called not System.exit()
            System.exit(0);
        }
        
        
        
    };
    
    private void initFrame() {
        this.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        this.addWindowListener(this.windowAdapter);
    }
    
    public ClosableFrame() {
        super("Closable frame demo");
        this.setPreferredSize(new Dimension(400, 400));
        this.initFrame();
    }
    
    private boolean isDataSaved() {
        // this should contain some real logic
        return false;
    }
    
}

The windowClosed(..) method handles the WINDOW_CLOSED event which is triggered when the window is closed using the dispose() method. If you are using it to terminate the application you should play nice and release all application resources and close all other frames that the application uses.

One more thing, if you want to close your frame with no questions asked you can use the dispose() method, but remember that it will trigger the WINDOW_CLOSED event and, therefore, it will terminate the application if you put application killing code into the windowClosed(..) event handler.