I have written this code (self contained) which throws an NPE that has me confounded. It involves a 2D array of Ray
objects used in a model. The Ray
objects are initialized and set to the model. When they are required outside the model, the code calls model.getRays()
. At that point the code iterates the 2D array & logs each object, because.. A mere 2-3 lines after the code calls getRays()
, the first element of the 2D array is null
!
There are no threads happening that might interfere with the objects in the array, so I am confounded as to how they could be null
only moments after testing that they contain valid objects.
What is wrong with this code & how do I fix it?
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.geom.*;
import java.awt.image.BufferedImage;
import java.util.logging.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
public class TestNullOn2DArray {
public static void main(String[] args) {
Runnable r = new Runnable() {
@Override
public void run() {
GUI gui = new GUI();
JFrame f = new JFrame("Test Null On 2D Array");
f.add(gui.getContainer());
f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
f.setLocationByPlatform(true);
f.pack();
f.setMinimumSize(f.getSize());
f.setVisible(true);
gui.plot();
}
};
SwingUtilities.invokeLater(r);
}
}
class GUI {
Logger log = Logger.getAnonymousLogger();
private JPanel container;
JLabel output;
private JMenuBar menuBar;
private BufferedImage canvas;
private CustomModel model =
new CustomModel(5, 1, .66f);
private int defaultPositionNumber = 5;
private int defaultAngleNumber = 5;
public void plot() {
Ray[][] rays = model.getRays();
log.log(Level.INFO, "Rays: " + rays);
for (int ii = 0; ii < rays.length; ii++) {
for (int jj = 0; jj < rays[ii].length; jj++) {
Ray ray = rays[ii][jj];
log.log(Level.INFO, "Ray: " + ray);
ray.resolve();
}
}
}
public JPanel getContainer() {
if (container == null) {
container = new JPanel(new BorderLayout(5, 5));
container.setBorder(new EmptyBorder(4, 4, 4, 4));
ImageIcon icon = new ImageIcon(getCanvas());
output = new JLabel(icon);
container.add(new JScrollPane(output), BorderLayout.CENTER);
}
return container;
}
public void refreshCanvas() {
model = new CustomModel(5, 6, .66f);
canvas = null;
BufferedImage bi = getCanvas();
output.setIcon(new ImageIcon(bi));
}
public void initializeRays(Graphics2D g) {
log.log(Level.INFO, "initializeRays()");
int startAngle = 1;
int numberAngles = defaultAngleNumber;
int inc = 90 / numberAngles;
int numberPositions = defaultPositionNumber;
Ray[][] rays = new Ray[numberAngles][numberPositions];
g.setTransform(AffineTransform.getTranslateInstance(0d, 0d));
for (int ii = rays.length - 1; ii > 0; ii--) {
for (int jj = 0; jj < rays[0].length; jj++) {
GeneralPath gp = new GeneralPath();
double rads = 2d * Math.PI * (startAngle + (inc * ii)) / 360d;
double x = 400d;
double y = 100d;
double yStart = y - (Math.sin(rads) * 100d);
double xStart = x + (Math.cos(rads) * 100d);
float r = 2f * (float) ((rads / Math.PI));
float b = (float) jj / (float) rays[0].length;
Color color = new Color(r, 1 - r, b, .6f);
g.setColor(color);
g.drawLine((int) x, (int) y, (int) xStart, (int) yStart);
gp.moveTo(xStart, yStart);
gp.lineTo(x, y);
gp.closePath();
Ray ray = new Ray(gp, color);
log.log(Level.INFO, "" + ray);
rays[ii][jj] = ray;
}
}
model.setRays(rays);
if (output != null) {
output.repaint();
}
}
public BufferedImage getCanvas() {
if (canvas == null) {
Dimension size = model.getPreferredSize();
int w = size.width;
int h = size.height;
canvas = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = canvas.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, w, h);
g.setColor(new Color(125, 27, 155, 127));
initializeRays(g);
g.dispose();
}
return canvas;
}
}
class CustomModel {
private int width;
private int layers;
private float offset;
private Ray[][] rays;
private Logger log = Logger.getAnonymousLogger();
public CustomModel(int width, int layers, float offset) {
this.width = width;
this.layers = layers;
this.offset = offset;
}
public Line2D.Double getLineOfLasSegment() {
Line2D.Double line = new Line2D.Double(0d, 0d, 1d, 1d);
return line;
}
public Dimension getPreferredSize() {
int w = 600;
int h = 300;
Dimension prefSize = new Dimension(w, h);
return prefSize;
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getLayers() {
return layers;
}
public void setLayers(int layers) {
this.layers = layers;
}
public Ray[][] getRays() {
for (int ii = 0; ii < rays.length; ii++) {
for (int jj = 0; jj < rays[ii].length; jj++) {
log.log(Level.INFO, "Ray: " + this.rays[ii][jj]);
}
}
return rays;
}
public void setRays(Ray[][] rays) {
this.rays = rays;
for (int ii = 0; ii < rays.length; ii++) {
for (int jj = 0; jj < rays[ii].length; jj++) {
//Ray ray = rays[ii][jj];
this.rays[ii][jj] = rays[ii][jj];
log.log(Level.INFO, "Ray: " + this.rays[ii][jj]);
}
}
}
public float getOffset() {
return offset;
}
public void setOffset(float offset) {
this.offset = offset;
}
}
class Ray {
private final Logger logger = Logger.getAnonymousLogger();
private GeneralPath path;
private boolean started = false;
private Color color;
Ray(GeneralPath path, Color color) {
this.path = path;
this.color = color;
}
public void resolve() {
logger.log(Level.INFO, "..resolving.");
}
public GeneralPath getPath() {
return path;
}
public boolean isStarted() {
return started;
}
@Override
public String toString() {
String s = "Ray: "
+ " started=" + started;
return s;
}
}
Output
This shows the last part of the output checking what getRays()
will return, followed by the NPE.
// ...
Jun 29, 2013 4:56:55 PM CustomModel getRays
INFO: Ray: Ray: started=false
Jun 29, 2013 4:56:55 PM GUI plot
INFO: Rays: [[LRay;@1a9876e
Jun 29, 2013 4:56:55 PM GUI plot
INFO: Ray: null
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
at GUI.plot(TestNullOn2DArray.java:52)
at TestNullOn2DArray$1.run(TestNullOn2DArray.java:26)
at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:251)
at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:727)
at java.awt.EventQueue.access$200(EventQueue.java:103)
at java.awt.EventQueue$3.run(EventQueue.java:688)
at java.awt.EventQueue$3.run(EventQueue.java:686)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:76)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:697)
at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:242)
at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:161)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:150)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:146)
...
Looking at it through my debugger, it looks like everything at
ray[0]
is null. Everything else is present.So, digging through the trace, I believe to have found the reason that data is put there in the first place, and why the first row is empty:
We call
f.add(gui.getContainer());
. This seem innocuous in and of itself, but let's see whatgetContainer
is doing...We instantiate
ImageIcon icon = new ImageIcon(getCanvas());
. Again, innocuous, but let's go down the rabbit hole again...We then call (finally)
initializeRays(g);
ingetCanvas()
! What's going wrong here? This:for (int ii = rays.length - 1; ii > 0; ii--)
Okay, you're counting backwards. Weird, and only should be done in rare cases, but I suppose you have your reasons. But, there's two catches to counting backwards:
Change it to include the greater-than or equal-to test, and you will not have any more NPEs.