If..Else 语句在 JFrame 程序中行为不可预测(不确定?)

If..Else statement behaving unpredictably (indeterministically?) in a JFrame program

编辑:一位用户标记我的问题可能与这个问题重复:“What is the volatile keyword useful for”,其标题是 "What is the volatile keyword useful for?"。我阅读了问题,但看不出它与我的问题有什么关系。


这是一个用两个 .java 文件编写的程序。 我的问题涉及 main 方法中的 if..else..

注意在下面的代码中,else {..} 中的单行被注释掉了。我将调用此程序的 "version 1",我将调用带有在 "version 2".

中注释的那一行的程序
// -------------
// The code below is in IfElseBugProgram.java
public class IfElseBugProgram {

    public static void main(String[] args) {

        MyJFrame terminal = new MyJFrame();

        while (true) {
            String keyReleased = terminal.getKeyReleased();

            if (! keyReleased.equals("") )
            {
                System.out.print("@" + keyReleased);
            }
            else
            {
                // System.out.print("!" + keyReleased);
            }

        } 
    }
}



// ----- 
//The code below is in file MyJFrame.java

import javax.swing.JFrame;

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

import java.util.ArrayList;
import java.util.List;


public class MyJFrame extends JFrame implements KeyListener
{
    private List<KeyEvent> keyEventQueue;

    public MyJFrame()
    {
        keyEventQueue = new ArrayList<KeyEvent>();

        this.addKeyListener(this);
        pack();

        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);
    }

    public void keyPressed(KeyEvent e)
    {
    }

    public void keyTyped(KeyEvent e)
    {
    }

    public void keyReleased(KeyEvent keyEvent)
    {
        keyEventQueue.add(keyEvent);
        System.out.println("some key was released!" + keyEventQueue.size());
    }


    public String getKeyReleased()
    {
        int i = keyEventQueue.size();

        if (i == 0)
        {
            return ("");
        }
        else
        {
            return ("" + i);
        }
    }

}

我希望 if {...} 中的代码是 运行,一旦我按下键盘上的某个键。也就是说,我期望 System.out.print("@" + keyReleased); 一旦我按下一个键,代码就是 运行。

对于版本 1,我 从来没有 似乎 System.out.print("@" + keyReleased); 变成了 运行;控制台中永远不会打印“@1”或“@2”或“@3”等。

对于版本 2(即,else {..} 块中的代码重新注释),

问题:为什么 if {..} 中的 System.out.print("@" + keyReleased); 行在版本 1 中不阻塞 运行,但(通常)在版本 2 中阻塞?


补充观察:

  1. 无论我如何更改代码,MyJFrame#keyReleased() 中的打印语句 System.out.println("some key was released!" + keyEventQueue.size()); 总是打印出来。

  2. 我认为我的 Netbeans 控制台可能有问题。但是我尝试了 运行ing if {..} 块中的其他代码,例如代码行 java.awt.Toolkit.getDefaultToolkit().beep();,它发出了声音。我还尝试在 JFrame 本身中显示一些图形。结果是一样的:把 else {...} 中的那行代码注释掉,if {..} 块中的代码似乎没有 运行;但是如果我把 else {..} 中的代码行重新注释掉,if {...} 块中的代码(包括发出声音或在 JFrame 中显示某些内容)实际上会 运行。

  3. 我很确定 if if (! keyReleased.equals("") ) 的条件是正确的。我尝试在 if (! keyReleased.equals("") ) 上方添加一行代码 System.out.println("?" + (! keyReleased.equals("")) );,并且始终在我的控制台上重复打印“?false”,直到我按下一个键,此时我得到“? true”重复打印。

    而且:奇怪的是,在版本 1 中将这一行代码 (System.out.println("?" + (! keyReleased.equals("")) );) 放在 if 之上会导致以下行if {..} 块中的代码到现在 运行?!


(可选)背景:

这部分解释了我最终要编写的代码。如果您希望提出一种可能使我实现此目标的方法(与我上面使用的方法不同),请随时提出建议。

我正在尝试构建一个名为 MyJFrame 的简单 class,它可以帮助激励朋友学习如何编程。这个想法是让他通过编写极其简单的游戏,以一种视觉激励的方式学习变量,就像我作为 child 学习 BASIC 时学到的那样。我希望他能够编写一个程序,完全包含在一个 main() 方法中;我希望他有一种在屏幕上绘制字符串的简单方法,以及一种获取用户输入的简单方法。示例程序可能如下所示:

int playerLocationX = 3;
int playerLocationY = 4;

MyJFrame terminal = new MyJFrame();

while(true)
{
  // erase player that was drawn during the previous iteration of this loop
  terminal.write(" ", playerLocationX, playerLocationY);

  // get a key the user last pressed (if the user pressed a key) and update 
  // player location

  String keyReleased = terminal.getKeyReleased();

  if (keyReleased.equals("LEFT"))
  {
    playerLocationX = playerLocationX - 1;
  }
  else if (keyReleased.equals("RIGHT"))
  {
    playerLocationY = playerLocationY + 1;
  }

  // draw player again, using most recent player location
  terminal.write("@", playerLocationX, playerLocationY);

}

我不想让他需要编写自己的keyReleased() 方法(即实现KeyListener 接口),因为这需要编写自己的方法的知识;它还需要了解 objects,因为 keyReleased() 方法无法修改存储在 main() 中的局部变量 playerLocationX 和 playerLocationY。

您遇到这种不可预测的行为的主要原因是因为您实际上创建了一个 multi-threaded 程序,您在其中从 事件调度线程

特别是行:

MyJFrame terminal = new MyJFrame();

启动 Swing 事件调度线程但是行(例如):

String keyReleased = terminal.getKeyReleased();

从主线程访问 terminal(一个 Swing 组件)。

来自Swing package documentation

In general Swing is not thread safe. All Swing components and related classes, unless otherwise documented, must be accessed on the event dispatching thread.

在继续尝试使此代码工作之前,我建议您阅读教程 Lesson: Concurrency in Swing

允许 Java 虚拟机通过假设变量不会被并发线程修改来优化连续的、非同步的加载。

如果你想在一个将被另一个线程更改的字段上进行自旋循环,你可以让虚拟机通过将其标记为 volatile:[=16 来确保每次读取都能看到最新的更改=]

private volatile List<KeyEvent> keyEventQueue;

As the JLS puts it:

A write to a volatile field (§8.3.1.4) happens-before every subsequent read of that field.

我不知道你的 V2 是否保证按照 JLS 工作,但是 System.out PrintStream 将在每次写入时同步,限制允许 VM 进行的优化.