java - 可观察到的并发修改异常
问题描述
我遇到了观察者设计模式的问题。
我试图自己实现像观察者这样的东西,但遇到了一个我不明白的严重错误。
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(Unknown Source)
at java.util.ArrayList$Itr.next(Unknown Source)
at userInterface.PauseScreen.acknowledgeChanges(PauseScreen.java:57)
at userInterface.PauseScreen$1.getPressed(PauseScreen.java:22)
at evo.EvoMain.update(EvoMain.java:76)
at org.newdawn.slick.GameContainer.updateAndRender(GameContainer.java:646)
at org.newdawn.slick.AppGameContainer.gameLoop(AppGameContainer.java:412)
at org.newdawn.slick.AppGameContainer.start(AppGameContainer.java:322)
at evo.EvoMain.main(EvoMain.java:32)
Sat May 26 17:28:31 CEST 2018 ERROR:Game.update() failure - check the game code.
org.newdawn.slick.SlickException: Game.update() failure - check the game code.
at org.newdawn.slick.GameContainer.updateAndRender(GameContainer.java:663)
at org.newdawn.slick.AppGameContainer.gameLoop(AppGameContainer.java:412)
at org.newdawn.slick.AppGameContainer.start(AppGameContainer.java:322)
at evo.EvoMain.main(EvoMain.java:32)
背景:我在 Slick2D 中用 Java 编写游戏,它的方向是暗黑破坏神。当我听说观察者设计模式时,我很兴奋,并尝试将其实现到我的游戏中,以便不同的组件可以轻松地相互通信。我希望暂停屏幕与游戏的 Main 类进行通信。我还希望 Main 类与玩家的状态屏幕进行通信。这两个通信路径主要是告诉那些组件做了什么输入,并得到他们按下的按钮作为反馈。我还希望 statscreen 与玩家交流。第一个:我希望统计屏幕告诉玩家他应该升级哪个统计第二个:我希望玩家返回他的统计(hp,hpRegeneration,stamina,staminaRegen,dmg)到统计屏幕,以便用户可以在那里查看它们并阅读数字。我想这可以在没有观察者的情况下实现,但我遇到了困难,并认为从长远来看,实现观察者并保持对象独立会更好。
所以给大家看几张图:
此时用户按下开始,游戏初始化。怪物和玩家已加载以及其他所有内容。
不幸的是,当“开始”按钮试图告诉游戏它被按下时,它会产生一个奇怪的 ConcurrentModificationError。对我来说更奇怪的是,错误只会发生,如果我将玩家添加到他/她的 statScreen 作为听众,这对我来说毫无意义,这似乎无关紧要。
编辑:有时当我在发生错误的代码行上放置一个断点时,该行在异常出现之前被触发 3 次。在前 2 次中断时,观察者 ArrayList 包含 EvoMain,但在第三次运行时,它也包含播放器。这是为什么? 为什么玩家是 PauseScreen 的观察者之一? 当我删除上面进一步提到的代码 行时导致错误出现的行。如果我停用该线路,则游戏将完美运行。 玩家没有出现在 ArrayList 中,游戏正常开始。难道是某种错误导致 ArrayList 在对象 pauseScreen 和 statScreen 之间共享?好像是静态的?!结束编辑。
我从这个版本的游戏中删除了大部分实际游戏内容,以隔离错误代码。这是我的课程:
EvoMain:
package evo;
import java.util.Hashtable;
import java.util.Map;
import org.lwjgl.input.Mouse;
import org.newdawn.slick.*;
import userInterface.EPauseScreenMessage;
import userInterface.PauseScreen;
import userInterface.StatScreen;
public class EvoMain extends BasicGame implements Observer {
private static Map<String, Image> imageCatalogue = new Hashtable<String, Image>();
private static Player player = null;
private static PauseScreen pauseScreen = null;
private static StatScreen statScreen = null;
private static GameContainer container;
private static int state = 0; // 0 = paused, 1 = running
public EvoMain() throws SlickException {
super("Evo");
}
public static void main(String[] args) throws SlickException {
AppGameContainer container = new AppGameContainer(new EvoMain());
container.setDisplayMode(1200, 900, false);
container.start();
}
@Override
public void render(GameContainer container, Graphics g) throws SlickException {
if (player != null) {
player.render(container, g);
statScreen.render(container, g);
}
pauseScreen.render(container, g);
}
@Override
public void init(final GameContainer container) throws SlickException {
fillImageCatalogue();
EvoMain.container = container;
pauseScreen = new PauseScreen(200, 100);
pauseScreen.add(this);
pauseScreen.setVisible(true);
pauseScreen.setEnabled(true);
statScreen = new StatScreen(0, 0, imageCatalogue.get("PlusButton"));
statScreen.add(this);
container.setMinimumLogicUpdateInterval(55);
container.setMaximumLogicUpdateInterval(55);
}
@Override
public void update(GameContainer container, int delta) throws SlickException {
Input input = container.getInput();
switch (state) {
case 0:
if(input.isKeyPressed(Input.KEY_ESCAPE)){
resumeGame();
break;
}
if (input.isKeyPressed(Input.KEY_ENTER)) {
resumeGame();
break;
}
if (input.isMouseButtonDown(0)) {
if (pauseScreen.isVisible() && pauseScreen.isEnabled()) {
for (Button button : pauseScreen.getButtons()) {
if (button.getRect().contains(Mouse.getX(), container.getHeight() - Mouse.getY())) {
// for some weird reason Mouse.getY() always gives the container height - the mouse' y coord so I have to reverse that by doing the same
button.getPressed();
}
}
}
}
break; // end of pause state
case 1:
if(input.isKeyPressed(Input.KEY_ESCAPE)){
state = 0;
pauseScreen.setVisible(true);
pauseScreen.setEnabled(true);
break;
}
if(input.isKeyPressed(Input.KEY_T)){
if (statScreen.isEnabled()) {
statScreen.setEnabled(false);
statScreen.setVisible(false);
} else {
statScreen.setVisible(true);
statScreen.setEnabled(true);
}
}
int dX = -1; // player destination X
int dY = -1; // player destination Y
if (input.isMouseButtonDown(0)) {
dX = Mouse.getX();
dY = container.getHeight() - Mouse.getY();
if (statScreen.isVisible() && statScreen.isEnabled()) {
for (Button button : statScreen.getButtons()) {
if (button.getRect().contains(dX, dY)) {
button.getPressed();
}
}
}
}
if (player != null) {
player.setDestination(dX, dY);
}
break; // end of running state
}
}
public void resumeGame() {
state = 1;
pauseScreen.setVisible(false);
pauseScreen.setEnabled(false);
}
public static void fillImageCatalogue() throws SlickException {
imageCatalogue.put("PlusButton" , new Image("res/PlusButton.jpg"));
}
public void initializeGame() {
EvoMain.player = new Player((float) 100, (float) 100);
statScreen.add(EvoMain.player);
// this line throws an exception in PauseScreen acknowledgeChanges();
}
public Player getPlayer() {
return player;
}
public void setPlayer(Player player) {
EvoMain.player = player;
}
@Override
public void update(Observable subject, Object object) {
if (subject instanceof PauseScreen) {
switch ((EPauseScreenMessage) object) {
case EXIT:
container.exit();
break;
case RESUME:
resumeGame();
break;
case START:
initializeGame();
resumeGame();
default:
break;
}
} else
if (subject instanceof StatScreen) {
if (object == null) {
statScreen.setVisible(false);
statScreen.setEnabled(false);
}
}
}
}
类播放器
package evo;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.SlickException;
import org.newdawn.slick.geom.Vector2f;
import userInterface.StatScreen;
public class Player extends Creature implements Observer, Observable {
private PlayerMessage pMessage = null;
private boolean enemyNearby = false;
public Player(float x, float y) {
super(x, y, 10.0, 9);
setAttackDmg(1.0);
}
@Override
public void render(GameContainer container, Graphics g) throws SlickException {
}
@Override
public void update(EvoMain game) {
}
public void calculateBodyWeight() {
}
public void setDestination(int x, int y) {
if (x != -1) {
destinationVector = new Vector2f(x - getPosition().getX(),y - getPosition().getY());
if (destinationVector.length() == 0.0) { // destination = player position ?
destinationVector = null; // no need to move then
}
} else {
destinationVector = null; // no destination active -> no vector needed
}
}
public boolean dealDamage(Creature creature) {
creature.setInvulnerableTicks(60);
if (creature.getHp() <= 0) {
kill(creature);
return true;
}
return false;
}
public void die() {
}
public double getHp() {
return hp;
}
public boolean hasEnemyNearby() {
return enemyNearby;
}
public void setEnemyNearby(boolean enemyNearby) {
this.enemyNearby = enemyNearby;
}
public void acknowledge(EStatMessage message, double value) {
pMessage = new PlayerMessage(message, value);
acknowledgeChanges();
}
@Override
public void acknowledgeChanges() {
for (Observer o : observers) {
o.update(this, pMessage);
}
}
public void add(Observer newObs) {
observers.add(newObs);
}
public void remove(Observer actObs) {
observers.remove(actObs);
}
@Override
public void update(Observable subject, Object object) {
if (subject instanceof StatScreen) {
}
}
@Override
public void kill(Creature creature) {
}
}
界面观察者
package evo;
public interface Observer {
void update(Observable subject, Object object);
}
接口可观察
package evo;
import java.util.ArrayList;
public interface Observable {
ArrayList<Observer> observers = new ArrayList<Observer>();
public void acknowledgeChanges();
public void add(Observer newObs);
public void remove(Observer actObs);
}
类暂停屏幕
package userInterface;
import org.newdawn.slick.Color;
import org.newdawn.slick.geom.Rectangle;
import evo.Observable;
import evo.Observer;
public class PauseScreen extends UserInterface implements Observable, Observer {
EPauseScreenMessage message;
public PauseScreen(float x, float y) {
super(x, y, 300, 400);
setMainColor(new Color(200, 200, 200));
setBorderColor(new Color(150, 150, 150));
TextButton btnStart = new TextButton(getX() + 20, getY() + 20, new Rectangle(getX() + 20, getY() + 20, 200, 75), "Start") {
@Override
public void getPressed() {
if (isEnabled()) {
message = EPauseScreenMessage.START;
acknowledgeChanges();
}
}
};
add(btnStart);
TextButton btnResume = new TextButton(getX() + 20, getY() + 120, new Rectangle(getX() + 20, getY() + 120, 200, 75), "Resume") {
@Override
public void getPressed() {
if (isEnabled()) {
message = EPauseScreenMessage.RESUME;
acknowledgeChanges();
}
}
};
add(btnResume);
TextButton btnExit = new TextButton(getX() + 20, getY() + 220, new Rectangle(getX() + 20, getY() + 220, 200, 75), "Exit game") {
@Override
public void getPressed() {
if (isEnabled()) {
message = EPauseScreenMessage.EXIT;
acknowledgeChanges();
}
}
};
btnExit.setTextColor(Color.red);
add(btnExit);
}
@Override
public void update(Observable subject, Object object) {
}
@Override
public void acknowledgeChanges() {
for (Observer obs : observers) {
obs.update(this, message);
}
}
@Override
public void add(Observer newObs) {
observers.add(newObs);
}
@Override
public void remove(Observer actObs) {
}
}
类 StatScreen
package userInterface;
import org.newdawn.slick.Color;
import org.newdawn.slick.Image;
import org.newdawn.slick.geom.Rectangle;
import evo.Observable;
import evo.Observer;
import evo.Player;
import evo.PlayerMessage;
public class StatScreen extends UserInterface implements Observer, Observable{
String stat = "1";
public StatScreen(float x, float y, Image plusButtonImage) {
super(x, y, 500, 550);
mainColor = new Color(200, 200, 200);
borderColor = new Color(150, 150, 150);
TextButton btnClose = new TextButton(getX() + 280, getY() + 460, new Rectangle(getX() + 280, getY() + 460, 200, 75), "Close") {
@Override
public void getPressed() {
if (isEnabled()) {
stat = "2";
acknowledgeChanges();
}
}
};
this.add(btnClose);
}
@Override
public void acknowledgeChanges() {
for (Observer observer : observers) {
observer.update(this, stat);
}
}
@Override
public void add(Observer newObs) {
observers.add(newObs);
}
@Override
public void remove(Observer actObs) {
observers.remove(actObs);
}
@Override
public void update(Observable subject, Object object) {
if (subject instanceof Player) {
PlayerMessage pM = (PlayerMessage) object;
switch (pM.getStat()) {
default:
break;
}
}
}
public String prepareValue(double value) {
return (int) value + "." + (int) (value * 10);
}
}
类用户界面
package userInterface;
import java.util.ArrayList;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.SlickException;
import evo.Button;
import evo.EvoObject;
public abstract class UserInterface extends EvoObject {
boolean visible = false;
boolean enabled = false;
int width;
int height;
Color mainColor;
Color borderColor;
ArrayList<Button> buttons = new ArrayList<Button>();
public UserInterface(float x, float y, int width, int height) {
super(x, y);
this.width = width;
this.height = height;
}
public void render(GameContainer container, Graphics g) throws SlickException {
if (visible) { // render menu if visible
g.setColor(mainColor);
g.fillRect(getX(), getY(), this.width, this.height);
g.setColor(borderColor);
g.setLineWidth(3);
g.drawRect(getX(), getY(), this.width, this.height);
for (Button button : buttons) { // render all buttons
button.render(container, g);
}
}
}
public boolean isVisible() {
return visible;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public Color getMainColor() {
return mainColor;
}
public void setMainColor(Color mainColor) {
this.mainColor = mainColor;
}
public Color getBorderColor() {
return borderColor;
}
public void setBorderColor(Color borderColor) {
this.borderColor = borderColor;
}
public void add(Button button) {
buttons.add(button);
}
public ArrayList<Button> getButtons() {
return buttons;
}
}
解决方案
接口使用错误导致的问题。
public interface Observable {
ArrayList<Observer> observers = new ArrayList<Observer>();
您在接口中声明一个列表,Observable
因此它变为,在和static
之间共享PauseScreen
StatScreen
当 时START
,您PauseScreen
通知您的观察者:
case START:
initializeGame();
resumeGame();
请注意,您正在PauseScreen
通过迭代器 foreach 迭代观察者,在此case Start
您将基础列表修改为方法add new Player()
中的可观察列表initializeGame
:
public void initializeGame() {
EvoMain.player = new Player((float) 100, (float) 100);
statScreen.add(EvoMain.player);
这将在 的循环中iter.next
调用时抛出并发修改异常。foreach
PauseScreen.acknowledgeChanges()
解决这个问题很容易:ArrayList<Observer> observers
从你的Observable
接口中删除并在实现类(PauseScreen
,StatScreen
)中创建
推荐阅读
- php - 如何在 PHP 中运行 Python 脚本而不使用 `exec()`、`system()` ...?
- html - 如何实现缩小的等高图像列
- c# - 如何使用 VLC Control C# 从视频中删除黑条
- kubernetes - Kubernetes 中的 Jfrog Artifactory 高可用设置
- docusignapi - DocuSign Rest API - 使用 CompositeTemplates 后获取信封状态文档
- asp.net-mvc - System.Web.dll 中出现“System.InsufficientExecutionStackException”类型的异常,但在 nopcommerce 3.80 的用户代码中未处理
- java - 使用 Java 8 流对象将列表对象转换为自定义 Map
- avro - AvroData 用架构默认值替换空值
- javascript - 立即停止 Puppeteer 删除文本
- javascript - 使用自动 ID 增量将数据保存在数组中