简体   繁体   中英

Removing object from a collection while iterating over it

I'm trying to remove objects that are outside of the JPanel. However, when I do that I get this error

在此处输入图片说明

and my program crashes. I was told by my lecturer that it's because two Threads are accessing the ArrayList that stores my objects.

I did to synchronize the functions but it didn't work.

Bullet

public void move(){
        if(y< -height){
            synchronized (this) {
                bullet.remove(this);
            }
        }
        y-=5;
    }

Relevant Classes:

Application

import javax.swing.*;

public class Application {
    public static String path ="C:\\Users\\jarek\\OneDrive\\NUIG Private\\(2) Semester 2 2019\\Next Generation Technologies II CT255\\Assignment 3\\";
    private Application(){
        JFrame frame = new JFrame("Ihsan The Defender");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        GamePanel gamePanel= new GamePanel();
        frame.add(gamePanel);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setResizable(false);
        frame.setVisible(true);
        new Thread(gamePanel).start();
    }

    public static void main (String args[]){
        new Application();
    }
}

GamePanel

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.security.Key;
import java.util.ArrayList;

public class GamePanel extends JPanel implements Runnable, KeyListener{
    private String path = Application.path;
    Image gameOverImg = new ImageIcon(path+"//images//gameover1.png").getImage();
    private Ihsan ihsan;
    private ArrayList <David> david = new ArrayList<>();
    private int enemies=5;
    private boolean pause=false;
    private boolean gameOver=false;

    GamePanel(){
        ihsan = new Ihsan(this);
        for(int i=0; i<enemies; i++){
            david.add(new David(this));
        }
        setFocusable(true);
        requestFocusInWindow();
        addKeyListener(this);
    }

    @Override
    public void run() {
        while (!pause){
            repaint();
            for(David david:david){
                david.move();
            }
            for(Bullet bullet:Bullet.bullet){
                bullet.move();
            }
            try{Thread.sleep(30);}
            catch (InterruptedException e){}
        }
    }

    public void paint(Graphics g){
        Graphics2D g2d = (Graphics2D) g.create();
        g2d.setColor(Color.GRAY);
        g2d.fillRect(0,0 ,getWidth(), getHeight());

        for(David david : david){
            g2d.drawImage(david.getImg(), david.getX(), david.getY(), null);
        }
        g2d.drawImage(ihsan.getImg(), ihsan.getX(), ihsan.getY(), null);
        for (Bullet bullet:Bullet.bullet){
            g2d.drawImage(bullet.getImg(), bullet.getX(), bullet.getY(), null);
        }

        if(gameOver){
            g2d.drawImage(gameOverImg,0,getHeight()/4,null);
        }
    }

    private static final Dimension DESIRED_SIZE = new Dimension(600,700);
    @Override
    public Dimension getPreferredSize(){
        return DESIRED_SIZE;
    }

    public void setGameOver(boolean gameOver) {
        this.gameOver = gameOver;
    }

    @Override
    public void keyPressed(KeyEvent e) {
        int key=e.getKeyCode();

        if (key==KeyEvent.VK_D || key==KeyEvent.VK_RIGHT){
            ihsan.move(4,0);
            System.out.println("Right Key");
        }

        if (key==KeyEvent.VK_A || key== KeyEvent.VK_LEFT){
            ihsan.move(-4,0);
            System.out.println("Left Key");
        }

        if(key==KeyEvent.VK_SPACE){
            Bullet.bullet.add(new Bullet(this,ihsan.getX()+(ihsan.getWidth()/2), ihsan.getY()));
        }
    }

    @Override
    public void keyTyped(KeyEvent e) { }

    @Override
    public void keyReleased(KeyEvent e) { }

    public boolean getGameOver(){
        return gameOver;
    }
}

Bullet

import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;

public class Bullet {
    //Environment
    public static ArrayList<Bullet> bullet = new ArrayList<>();
    private String path = Application.path;
    private GamePanel gp;
    //properties
    private int x,y;
    private int width,height;
    private int yVector;
    private Image image;


    Bullet(GamePanel gp, int x, int y){
        image = new ImageIcon(path+"\\images\\javaicon.png").getImage();
        width=image.getWidth(null);
        height=image.getHeight(null);
        this.gp=gp;
        this.x=x;
        this.y=y;
        yVector=5;
    }

    public void move(){
        if(y< -height){
           bullet.remove(this);
        }
        y-=5;
    }

    public Image getImg(){
        return image;
    }

    public int getX(){
        return x;
    }

    public int getY(){
        return y;
    }
}

The synchronized block must be around every piece of code that accessed or modifies the ArrayList . The object put in the parenthesis must be the same: It's the lock.

Create a field of type Object named bulletLock for example, and use it as a lock, every time you access bullet .

The error occurs because you're removing a bullet while another thread is in a for-loop on the list. As there is a concurrent modification, it can't continue safely.

Another solution would be to make a copy of the ArrayList before your for-loop.

Your current problem is not with synchronization, but that you modify the bullet list while iterating over it:

// GamePanel.java#run():
    for (Bullet bullet:Bullet.bullet) { //your code is iterating over Bullet.bullet here
        bullet.move(); //you call Bullet#move here
    }

// Bullet.java#move():
    public void move(){
        if(y< -height){
           bullet.remove(this); //this will remove the current bullet from Bullet.bullet
           // ultimately causing the ConcurrrentModificationException in GamePanel.run()
        }
        y-=5;
    }

Synchronization won't help, since both actions occur within the same thread.

To solve this issue the Bullet.move() method needs to return a boolean indicating whether it should be removed from the list. And GamePanel.run() must not use an enhanced for loop but an iterator (removing an element from a list using Iterator.remove() is safe if this is the only active Iterator ):

// Bullet.java#move():
    public boolean move(){
        if(y< -height){
           return true; // instruct GamePanel.run() to remove this bullet
        }
        y-=5;
        return false; // keep this bullet
    }

// GamePanel.java#run():
    Iterator<Bullet> it = Bullet.bullet.iterator();
    while (it.hasNext()) {
        Bullet bullet = it.next();
        if (bullet.move()) { // if bullet should be removed
            it.remove(); // remove it from the list
        }
    }

There are other issues too:

  • you call #repaint() from your own thread instead of the Swing EDT
  • repainting iterates over the same Bullet.bullet list without synchronization (which may lead to a ConcurrentModificationException within GamePanel.paint() )

A simple solution to the problem described in Thomas Kläger answer could be:

for(Bullet bullet:  new ArrayList(Bullet.bullet) ){ //iterate over a copy
       bullet.move();
}

Alternatively:

Iterator<Bullet> it = Bullet.bullet.iterator();
while (it.hasNext()) {
    Bullet bullet = it.next();
    bullet.move();
}

without changing other parts of the code.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM