我正在上一个类,其中包含一个Image并将其绘制为以0,0为中心;为此,它检索图像的高度和宽度,并将其显示偏移基于这些值。但是,如果将其设置为ImageObserver以防图像尚未完全加载,则会在构造函数中泄漏this:

public class Sprite extends SimplePaintable implements ImageObserver {

    private final Image sprite;
    private int xOffset;
    private boolean xOffsetSet;
    private int yOffset;
    private boolean yOffsetSet;

    public Sprite(Image sprite) {
        this.sprite = sprite;
        //warning: leaking this in constructor
        int width = sprite.getWidth(this);
        xOffset = width/2;
        xOffsetSet = width != -1;
        int height = sprite.getHeight(this);
        yOffset = height/2;
        yOffsetSet = height != -1;
    }

    @Override
    public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
        assert img == sprite;
        if ((infoflags & WIDTH) != 0) {
            xOffset = width / 2;
            xOffsetSet = true;
        }
        if ((infoflags & HEIGHT) != 0) {
            yOffset = height / 2;
            yOffsetSet = true;
        }
        return !(xOffsetSet && yOffsetSet);
    }
    ...

一开始我认为这很好,因为仅偏移量变量未初始化,并且其默认值适合(不)显示未加载的图像,但是后来我意识到,如果立即调用图像作为getWidth(this)加载的图像,则理论上可以调用imageUpdate在构造函数完成之前,导致偏移量在imageUpdate中正确设置,然后由构造函数取消设置。这是一个问题,还是仅在EDT上同步加载图像?如果这是一个问题,那么使imageUpdate成为synchronized方法会阻止它在构造函数完成之前运行吗?

最佳答案

构造函数中的this泄漏只是警告,因为它可能会导致问题。如果super变量被初始化,但是this之后对其进行了修改,但是super自身泄漏了,那么有人正在使用尚未完全初始化的变量。

如果您确定在构造时不会有人访问任何变量,并且没有其他方法可以做到这一点(因为外部库阻止您以正确的方式进行操作),那么可以忽略该警告。

在您的情况下,从理论上讲,在调用sprite.getWidth(this)时,图像会调用观察者以立即更新进度,因此在构造函数完成之前调用imageUpdate。在这种情况下,偏移量变量将在以后由构造函数初始化覆盖。不,同步不会阻止该问题,因为在此刻没有其他人握住该锁。

有几种方法可以解决此问题:

  • 使用BufferedImage,它不需要getWidth/getHeight的观察者。缺点:必须完全加载镜像,在某些情况下,这可能会导致较小的延迟(如果通过网络加载)。
  • 用户正确锁定:
    private final Object offsetLock = new Object();
    
    public Sprite(Image sprite) {
        this.sprite = sprite;
    
        synchronized(offsetLock) {
            int width = sprite.getWidth(this);
            xOffset = width/2;
            xOffsetSet = width != -1;
            int height = sprite.getHeight(this);
            yOffset = height/2;
            yOffsetSet = height != -1;
        }
    }
    
    @Override
    public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
        assert img == sprite;
        if ((infoflags & WIDTH) != 0) {
            synchronized(offsetLock) {
                xOffset = width / 2;
                xOffsetSet = true;
            }
        }
        if ((infoflags & HEIGHT) != 0) {
            synchronized(offsetLock) {
                yOffset = height / 2;
                yOffsetSet = true;
            }
        }
        return xOffsetSet && yOffsetSet;
    }
    

    缺点:这将调用同步,这可能会导致一些延迟。您通常不会注意到是否只调用了几次,但是在时间紧迫的循环中这可能会很明显。
  • 在构造完成之后,使用外部函数来更新数据:
    public Sprite(Image sprite) {
        this.sprite = sprite;
    }
    
    protected updateOffsets() {
        updateWidth(sprite.getWidth(this));
        updateHeight(sprite.getHeight(this));
    }
    
    protected updateWidth(final int newWidth) {
        if (newWidth != -1) {
            xOffset = newWidth/2;
            xOffsetSet = true;
        }
    }
    
    protected updateHeight(final int newHeight) {
        if (newHeight!= -1) {
            yOffset = newHeight/2;
            yOffsetSet = true;
        }
    }
    
    @Override
    public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
        assert img == sprite;
        if ((infoflags & WIDTH) != 0) {
            updateWidth(width);
        }
        if ((infoflags & HEIGHT) != 0) {
            updateHeight(height);
        }
        return xOffsetSet && yOffsetSet;
    }
    

    缺点:有人必须调用updateOffsets()方法,并且在此对象未完全初始化之前,这可能会导致错误或要求您编写一个builder方法,从而使事情更加复杂。
  • 10-01 02:38
    查看更多