
理解问题:为何只显示最后一个图形?
在java swing应用程序中,尤其是在自定义绘图组件时,一个常见的问题是,即使我们向绘图列表中添加了多个图形对象,最终屏幕上却只显示最后绘制的那一个,或者所有图形都重叠在同一个位置。这通常不是绘图逻辑本身的错误,而是由于对java中对象引用传递机制的误解。
以本案例为例,Painter 类中定义了两个 Point 类型的成员变量 startPoint 和 endPoint:
public class Painter implements ActionListener, MouseListener, MouseMotionListener {
// ...
Point startPoint = new Point(); // 初始化的Point对象
Point endPoint = new Point(); // 初始化的Point对象
// ...
}在鼠标事件处理方法 mousePressed 和 mouseReleased 中,这些 Point 对象的坐标会被更新:
@Override
public void mousePressed(MouseEvent e) {
startPoint.setLocation(e.getPoint()); // 更新现有startPoint对象的坐标
}
@Override
public void mouseReleased(MouseEvent e) {
endPoint.setLocation(e.getPoint()); // 更新现有endPoint对象的坐标
if (object == 0) {
canvas.addPrimitive(new Line(startPoint, endPoint, temp)); // 将startPoint和endPoint传递给Line对象
}
// ...
}问题就出在这里。startPoint 和 endPoint 在 Painter 类的生命周期中只被初始化了一次。每次鼠标按下或释放时,setLocation() 方法仅仅是修改了这两个 现有 Point 对象的内部坐标值,而不是创建新的 Point 对象。
当 canvas.addPrimitive(new Line(startPoint, endPoint, temp)) 被调用时,Line 类的构造函数接收的是 Painter 类中 startPoint 和 endPoint 这两个 相同 Point 对象的引用。这意味着,无论你创建多少个 Line 或 Circle 对象,它们内部存储的 startPoint 和 endPoint 引用都指向 Painter 类中那两个唯一的 Point 实例。
立即学习“Java免费学习笔记(深入)”;
因此,每次用户绘制新图形时,Painter 类的 startPoint 和 endPoint 会被更新到最新的鼠标位置。由于所有先前创建的 Line 或 Circle 对象都引用着这两个相同的 Point 实例,当 paintComponent 方法被调用并遍历 primitives 列表进行绘制时,所有图形都会根据 startPoint 和 endPoint 的 当前 值(即最后一次鼠标释放时的值)进行绘制,从而导致所有图形都重叠在最后绘制的位置。
解决方案:确保每个图形拥有独立的坐标
要解决这个问题,核心思想是确保每个绘制的图形对象都拥有其自己独立的坐标数据,而不是共享同一个 Point 实例。这可以通过两种方式实现:
方法一:在事件处理器中创建新的 Point 实例
最直接的解决方案是在 mousePressed 和 mouseReleased 方法中,每次都创建新的 Point 对象来存储当前的鼠标位置,而不是修改现有的 startPoint 和 endPoint 成员变量。
修改 Painter 类中的 mousePressed 和 mouseReleased 方法如下:
public class Painter implements ActionListener, MouseListener, MouseMotionListener {
// ...
// startPoint 和 endPoint 仍然可以是成员变量,但现在它们将在每次事件中被重新赋值为新对象
Point startPoint;
Point endPoint;
// ...
@Override
public void mousePressed(MouseEvent e) {
// 创建一个新的 Point 实例来存储鼠标按下的位置
startPoint = new Point(e.getPoint());
}
@Override
public void mouseReleased(MouseEvent e) {
// 创建一个新的 Point 实例来存储鼠标释放的位置
endPoint = new Point(e.getPoint());
if (object == 0) {
// 现在传递给 Line 构造器的是新创建的、独立的 Point 对象
canvas.addPrimitive(new Line(startPoint, endPoint, temp));
}
if (object == 1){
// 同样,Circle 也会接收到独立的 Point 对象
canvas.addPrimitive(new Circle(startPoint, endPoint, temp));
}
canvas.repaint();
}
// ...
}通过这种修改,每次鼠标事件发生时,startPoint 和 endPoint 都会指向一个新的 Point 对象。当这些新的 Point 对象被传递给 Line 或 Circle 的构造器时,每个图形实例将拥有其自己独立的坐标数据,不再受后续鼠标事件的影响。
方法二:在图形构造器中进行防御性拷贝
作为一种防御性编程实践,即使 Painter 类已经创建了新的 Point 实例,在 Line(以及 Circle)的构造器中对传入的 Point 对象进行“防御性拷贝”也是一个好习惯。这意味着,Line 对象不会直接存储传入的 Point 引用,而是创建一个新的 Point 对象,并用传入 Point 的值进行初始化。
这样做的好处是,即使外部代码(例如 Painter 类)不小心修改了传递给 Line 构造器的 Point 对象,Line 实例内部存储的坐标也不会受到影响,从而保证了图形的独立性和数据完整性。
修改 Line 类(以及 Circle 类)的构造器如下:
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Color;
public class Line extends PaintingPrimitive{
Point startPoint; // 声明为成员变量,但不在声明时初始化,而是在构造器中初始化
Point endPoint;
public Line(Point start, Point end, Color c) {
super(c);
// 对传入的 Point 对象进行防御性拷贝,确保 Line 实例拥有独立的 Point 数据
this.startPoint = new Point(start);
this.endPoint = new Point(end);
}
public void drawGeometry(Graphics g) {
System.out.println("draw geo called");
g.drawLine(startPoint.x, startPoint.y, endPoint.x, endPoint.y);
}
@Override
public String toString() {
return "Line";
}
}通过这种修改,Line 对象内部持有的 startPoint 和 endPoint 将是其私有的副本,与外部任何 Point 对象都无关。这提供了更强的封装性和健壮性。
整合修改后的关键代码示例
为了清晰起见,以下是整合了上述两种修改方案的关键代码片段:
Painter 类 (部分修改)
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.util.ArrayList; // 假设 PaintingPanel 在同一文件或可访问
public class Painter implements ActionListener, MouseListener, MouseMotionListener {
// ... 其他成员变量
Color temp = Color.RED;
int object = 0; // 0 = line, 1 = circle
PaintingPanel canvas;
// 不再在声明时初始化,而是在 mousePressed/Released 中赋值新对象
Point startPoint;
Point endPoint;
Painter() {
// ... 构造器中的其他UI初始化代码
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(500, 500);
// ... 其他面板和按钮设置
canvas = new PaintingPanel();
// ... 将 canvas 添加到 holder
// 注册监听器
// ... 按钮监听器
canvas.addMouseListener(this); // 将鼠标监听器添加到 canvas 上
// ...
frame.setContentPane(holder);
frame.setVisible(true);
}
// ... actionPerformed, mouseDragged, mouseMoved, mouseClicked, mouseEntered, mouseExited 方法
@Override
public void mousePressed(MouseEvent e) {
// 关键修改:每次按下鼠标时创建新的 Point 对象
startPoint = new Point(e.getPoint());
}
@Override
public void mouseReleased(MouseEvent e) {
// 关键修改:每次释放鼠标时创建新的 Point 对象
endPoint = new Point(e.getPoint());
if (object == 0) {
canvas.addPrimitive(new Line(startPoint, endPoint, temp));
}
if (object == 1){
canvas.addPrimitive(new Circle(startPoint, endPoint, temp));
}
canvas.repaint(); // 通知 PaintingPanel 重新绘制
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new Painter()); // 推荐在事件分发线程中创建UI
}
}Line 类 (部分修改)
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Color;
public class Line extends PaintingPrimitive{
Point startPoint; // 不再在声明时初始化
Point endPoint; // 不再在声明时初始化
public Line(Point start, Point end, Color c) {
super(c);
// 关键修改:在构造器中进行防御性拷贝
this.startPoint = new Point(start);
this.endPoint = new Point(end);
}
public void drawGeometry(Graphics g) {
// System.out.println("draw geo called"); // 调试信息可以移除
g.drawLine(startPoint.x, startPoint.y, endPoint.x, endPoint.y);
}
@Override
public String toString() {
return "Line";
}
}PaintingPanel 类 (无需修改,但为了完整性列出)
import java.util.ArrayList;
import javax.swing.JPanel;
import java.awt.Graphics;
import java.awt.Color;
public class PaintingPanel extends JPanel {
ArrayList primitives = new ArrayList();
PaintingPanel() {
setBackground(Color.WHITE);
}
public void addPrimitive(PaintingPrimitive obj) {
primitives.add(obj);
// this.repaint(); // 可以在 Painter 中统一调用,或者在这里调用,确保UI更新
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g); // 始终调用父类的 paintComponent 来清空背景等
for (PaintingPrimitive shape : primitives) {
// g.drawLine(0,0,100,100); // 这行调试代码可以移除
shape.draw(g); // 调用每个图形的 draw 方法
}
}
} 注意事项
- 对象引用与值传递: 理解Java中对象是按引用传递的至关重要。当一个对象作为参数传递给方法时,实际传递的是该对象的引用(地址),而不是对象本身的副本。因此,在方法内部对对象属性的修改会影响到原始对象。
- 防御性编程: 在构造器中进行防御性拷贝是一个良好的编程习惯,尤其是在处理可变对象(如 Point)时。它能有效防止外部对对象内部状态的意外修改,增强代码的健壮性和可维护性。
- 性能考虑: 每次创建新的 Point 对象会带来轻微的内存分配和垃圾回收开销。然而,对于大多数交互式绘图应用而言,这种开销通常可以忽略不计。只有在绘制极其大量的微小图形(例如每秒成千上万个)时,才需要考虑对象池或其他优化策略。
- SwingUtilities.invokeLater: 在 main 方法中创建 Swing UI 组件时,推荐使用 SwingUtilities.invokeLater(() -> new Painter());。这确保了UI组件的创建和更新都在事件分发线程(Event Dispatch Thread, EDT)上进行,避免潜在的线程安全问题。
总结
只显示最后一个图形的问题,通常是由于Java中对对象引用传递机制理解不足导致的。通过在鼠标事件处理器中每次创建新的 Point 实例,以及在图形构造器中进行防御性拷贝,我们可以确保每个图形对象都拥有独立的坐标数据,从而正确地在 JPanel 上显示所有绘制的图形。掌握这些概念对于开发健壮和可维护的Java Swing绘图应用程序至关重要。











