Java 中文官方教程 2022 版(二十六)
监听器 API 表
在下表中,第一列给出了监听器接口的名称,其中包含指向讨论该监听器的教程页面的链接,或者如果教程未讨论该监听器,则包含指向 API 文档的链接。第二列列出了相应的适配器类(如果有)。(有关使用适配器的讨论,请参见使用适配器和内部类处理事件。)第三列列出了监听器接口包含的方法,并显示传递到方法中的事件对象的类型。通常,监听器、适配器和事件类型具有相同的名称前缀,但并非总是如此。
要查看哪些 Swing 组件可以触发哪些类型的事件,请参见 Swing 组件支持的监听器。
| 监听器接口 | 适配器类 | 监听器方法 |
|---|---|---|
动作监听器 |
无 | actionPerformed(ActionEvent) |
| 祖先监听器 | 无 | ancestorAdded(AncestorEvent) ancestorMoved(AncestorEvent)
ancestorRemoved(AncestorEvent) |
插入符监听器 |
无 | caretUpdate(CaretEvent) |
|---|---|---|
单元编辑器监听器 |
无 | editingStopped(ChangeEvent) editingCanceled(ChangeEvent) |
变更监听器 |
无 | stateChanged(ChangeEvent) |
| 组件监听器 | 组件适配器 | componentHidden(ComponentEvent) componentMoved(ComponentEvent)
componentResized(ComponentEvent)
componentShown(ComponentEvent) |
容器监听器 |
容器适配器 |
componentAdded(ContainerEvent) componentRemoved(ContainerEvent) |
|---|
| 文档监听器 | 无 | changedUpdate(DocumentEvent) insertUpdate(DocumentEvent)
removeUpdate(DocumentEvent) |
异常监听器 |
无 | exceptionThrown(Exception) |
|---|---|---|
焦点监听器 |
焦点适配器 |
focusGained(FocusEvent) focusLost(FocusEvent) |
层次边界监听器 |
层次边界适配器 |
ancestorMoved(HierarchyEvent) ancestorResized(HierarchyEvent) |
层次监听器 |
无 | hierarchyChanged(HierarchyEvent) |
超链接监听器 |
无 | hyperlinkUpdate(HyperlinkEvent) |
InputMethodListener |
none | caretPositionChanged(InputMethodEvent) inputMethodTextChanged(InputMethodEvent) |
| InternalFrameListener | InternalFrameAdapter | internalFrameActivated(InternalFrameEvent) internalFrameClosed(InternalFrameEvent)
internalFrameClosing(InternalFrameEvent)
internalFrameDeactivated(InternalFrameEvent)
internalFrameDeiconified(InternalFrameEvent)
internalFrameIconified(InternalFrameEvent)
internalFrameOpened(InternalFrameEvent) |
ItemListener |
none | itemStateChanged(ItemEvent) |
|---|
| KeyListener | KeyAdapter | keyPressed(KeyEvent) keyReleased(KeyEvent)
keyTyped(KeyEvent) |
| ListDataListener | none | contentsChanged(ListDataEvent) intervalAdded(ListDataEvent)
intervalRemoved(ListDataEvent) |
ListSelectionListener |
none | valueChanged(ListSelectionEvent) |
|---|
| MenuDragMouseListener | none | menuDragMouseDragged(MenuDragMouseEvent) menuDragMouseEntered(MenuDragMouseEvent)
menuDragMouseExited(MenuDragMouseEvent)
menuDragMouseReleased(MenuDragMouseEvent) |
| MenuKeyListener | none | menuKeyPressed(MenuKeyEvent) menuKeyReleased(MenuKeyEvent)
menuKeyTyped(MenuKeyEvent) |
| MenuListener | none | menuCanceled(MenuEvent) menuDeselected(MenuEvent)
menuSelected(MenuEvent) |
| MouseInputListener (extends MouseListener and MouseMotionListener | MouseInputAdapter MouseAdapter | mouseClicked(MouseEvent) mouseEntered(MouseEvent)
mouseExited(MouseEvent)
mousePressed(MouseEvent)
mouseReleased(MouseEvent)
mouseDragged(MouseEvent)
mouseMoved(MouseEvent)
MouseAdapter(MouseEvent) |
| MouseListener | MouseAdapter, MouseInputAdapter | mouseClicked(MouseEvent) mouseEntered(MouseEvent)
mouseExited(MouseEvent)
mousePressed(MouseEvent)
mouseReleased(MouseEvent) |
MouseMotionListener |
MouseMotionAdapter, MouseInputAdapter |
mouseDragged(MouseEvent) mouseMoved(MouseEvent) |
|---|---|---|
MouseWheelListener |
MouseAdapter |
mouseWheelMoved(MouseWheelEvent) MouseAdapter<MouseEvent> |
| PopupMenuListener | none | popupMenuCanceled(PopupMenuEvent) popupMenuWillBecomeInvisible(PopupMenuEvent)
popupMenuWillBecomeVisible(PopupMenuEvent) |
属性更改监听器 |
无 | propertyChange(PropertyChangeEvent) |
|---|
| 表列模型监听器 | 无 | columnAdded(TableColumnModelEvent) columnMoved(TableColumnModelEvent)
columnRemoved(TableColumnModelEvent)
columnMarginChanged(ChangeEvent)
columnSelectionChanged(ListSelectionEvent) |
表模型监听器 |
无 | tableChanged(TableModelEvent) |
|---|---|---|
树展开监听器 |
无 | treeCollapsed(TreeExpansionEvent) treeExpanded(TreeExpansionEvent) |
| 树模型监听器 | 无 | treeNodesChanged(TreeModelEvent) treeNodesInserted(TreeModelEvent)
treeNodesRemoved(TreeModelEvent)
treeStructureChanged(TreeModelEvent) |
树选择监听器 |
无 | valueChanged(TreeSelectionEvent) |
|---|---|---|
树将展开监听器 |
无 | treeWillCollapse(TreeExpansionEvent) treeWillExpand(TreeExpansionEvent) |
可撤销编辑监听器 |
无 | undoableEditHappened(UndoableEditEvent) |
可否更改监听器 |
无 | vetoableChange(PropertyChangeEvent) |
窗口焦点监听器 |
窗口适配器 |
windowGainedFocus(WindowEvent) windowLostFocus(WindowEvent) |
| 窗口监听器 | 窗口适配器 | windowActivated(WindowEvent) windowClosed(WindowEvent)
windowClosing(WindowEvent)
windowDeactivated(WindowEvent)
windowDeiconified(WindowEvent)
windowIconified(WindowEvent)
windowOpened(WindowEvent) |
窗口状态监听器 |
窗口适配器 |
windowStateChanged(WindowEvent) |
|---|
解决常见事件处理问题
原文:
docs.oracle.com/javase/tutorial/uiswing/events/problems.html
本节讨论您在处理事件时可能遇到的问题。
问题: 我试图处理组件的某些事件,但组件没有生成应该生成的事件。
-
首先,请确保您注册了正确类型的监听器以侦测事件。看看是否另一种类型的监听器可以检测到你需要的事件类型。
-
确保您在正确的对象上注册了监听器。
-
您是否正确实现了事件处理程序?例如,如果您扩展了适配器类,则确保使用了正确的方法签名。确保每个事件处理方法是
public void,名称拼写正确,参数类型正确。
问题: 我的组合框没有生成诸如焦点事件之类的低级事件。
- 组合框是复合组件 —— 使用多个组件实现的组件。因此,组合框不会触发简单组件触发的低级事件。有关更多信息,请参见在组合框上处理事件。
问题: 编辑器窗格(或文本窗格)的文档没有触发文档事件。
- 编辑器窗格或文本窗格的文档实例在从 URL 加载文本时可能会更改。因此,您的监听器可能在未使用的文档上监听事件。例如,如果您使用先前加载过纯文本的 HTML 加载编辑器窗格或文本窗格,则文档将更改为
HTMLDocument实例。如果您的程序动态将文本加载到编辑器窗格或文本窗格中,请确保代码调整以适应文档可能发生的更改(在新文档上重新注册文档监听器等)。
如果您在此列表中找不到您的问题,请参见解决常见组件问题。
问题和练习:编写事件监听器
译文:
docs.oracle.com/javase/tutorial/uiswing/QandE/questions-ch5.html
使用本课程的表格,组件操作部分和事件监听器操作部分来完成这些问题和练习。
问题
1. 当特定组件出现在屏幕上时,你会实现哪个监听器以便得到通知?哪个方法告诉你这个信息?
2. 当用户通过按 Enter 键完成编辑文本字段时,你会实现哪个监听器以便得到通知?当每个字符输入到文本字段时,你会实现哪个监听器以便得到通知?请注意,你不应该实现一个通用的键监听器,而是一个特定于文本的监听器。
3. 当微调器的值发生变化时,你会实现哪个监听器以便得到通知?你会如何获取微调器的新值?
4. 焦点子系统的默认行为是消耗焦点遍历键,比如 Tab 和 Shift Tab。假设你想要阻止这种情况发生在你应用程序的某个组件中。你会如何实现?
练习
1. 取出Beeper.java示例并添加一个文本字段。实现当用户完成输入数据时,系统发出蜂鸣声。
2. 取出Beeper.java示例并添加一个可选择的组件,允许用户输入从 1 到 10 的数字。例如,你可以使用一个组合框,一组单选按钮或一个微调器。实现当用户选择数字后,系统发出相应次数的蜂鸣声。
检查你的答案。
课程:执行自定义绘制
原文:
docs.oracle.com/javase/tutorial/uiswing/painting/index.html
这节课描述了 Swing 中的自定义绘制。许多程序可以很好地运行而无需编写自己的绘制代码;它们将简单地使用 Swing API 中已经可用的标准 GUI 组件。但是,如果您需要对图形的绘制方式进行特定控制,那么这节课就是为您准备的。我们将通过创建一个简单的 GUI 应用程序来探讨自定义绘制,该应用程序根据用户的鼠标活动绘制形状。通过故意保持其设计简单,我们可以专注于底层绘制概念,这将与您将来开发的其他 GUI 应用程序相关联。
这节课在构建演示应用程序时逐步解释每个概念。它尽可能快地呈现代码,最小化背景阅读量。在 Swing 中进行自定义绘制类似于在 AWT 中进行自定义绘制,但由于我们不建议完全使用 AWT 编写应用程序,因此这里不特别讨论其绘制机制。您可能会发现在阅读本课程后阅读文章《AWT 和 Swing 中的绘制》中的深入讨论很有用。
创建演示应用程序(第 1 步)
原文:
docs.oracle.com/javase/tutorial/uiswing/painting/step1.html
所有图形用户界面都需要某种主应用程序框架来显示。在 Swing 中,这是javax.swing.JFrame的一个实例。因此,我们的第一步是实例化这个类,并确保一切按预期工作。请注意,在 Swing 中编程时,你的 GUI 创建代码应该放在事件分发线程(EDT)上。这将防止潜在的竞争条件,可能导致死锁。以下代码清单显示了如何实现这一点。

一个javax.swing.JFrame的实例
单击“启动”按钮以使用Java™ Web Start运行 SwingPaintDemo1(下载 JDK 7 或更高版本)。或者,要自行编译和运行示例,请参考示例索引。
package painting;
import javax.swing.SwingUtilities;
import javax.swing.JFrame;
public class SwingPaintDemo1 {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
}
private static void createAndShowGUI() {
System.out.println("Created GUI on EDT? "+
SwingUtilities.isEventDispatchThread());
JFrame f = new JFrame("Swing Paint Demo");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setSize(250,250);
f.setVisible(true);
}
}
这将创建框架,设置其标题,并使一切可见。我们使用SwingUtilities辅助类在事件分发线程上构建此 GUI。请注意,默认情况下,当用户单击“关闭”按钮时,JFrame不会退出应用程序。我们通过调用setDefaultCloseOperation方法并传入适当的参数来提供此行为。此外,我们明确设置框架的大小为 250 x 250 像素。一旦我们开始向框架添加组件,这一步将不再必要。
练习:
-
编译并运行应用程序。
-
测试最小化和最大化按钮。
-
单击关闭按钮(应用程序应该退出)。
创建演示应用程序(第 2 步)
原文:
docs.oracle.com/javase/tutorial/uiswing/painting/step2.html
接下来,我们将向框架添加一个自定义绘图表面。为此,我们将创建一个 javax.swing.JPanel 的子类(一个通用的轻量级容器),它将提供渲染我们自定义绘图的代码。

一个 javax.swing.JPanel 子类
点击“启动”按钮以使用 Java™ Web Start 运行 SwingPaintDemo2(下载 JDK 7 或更高版本)。或者,要自行编译和运行示例,请参考 示例索引。
package painting;
import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.BorderFactory;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
public class SwingPaintDemo2 {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
}
private static void createAndShowGUI() {
System.out.println("Created GUI on EDT? "+
SwingUtilities.isEventDispatchThread());
JFrame f = new JFrame("Swing Paint Demo");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(new MyPanel());
f.pack();
f.setVisible(true);
}
}
class MyPanel extends JPanel {
public MyPanel() {
setBorder(BorderFactory.createLineBorder(Color.black));
}
public Dimension getPreferredSize() {
return new Dimension(250,200);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
// Draw Text
g.drawString("This is my custom Panel!",10,20);
}
}
你将注意到的第一个变化是我们现在导入了许多额外的类,比如 JPanel、Color 和 Graphics。由于一些旧的 AWT 类仍然在现代 Swing 应用程序中使用,所以在一些导入语句中看到 java.awt 包是正常的。我们还定义了一个名为 MyPanel 的自定义 JPanel 子类,它包含了大部分新代码。
MyPanel 类定义有一个构造函数,它在其边缘周围设置了黑色边框。这是一个细微的细节,一开始可能很难看到(如果看不到,只需注释掉 setBorder 的调用,然后重新编译)。MyPanel 还重写了 getPreferredSize,它返回面板的期望宽度和高度(在本例中,宽度为 250,高度为 200)。因此,SwingPaintDemo 类不再需要指定框架的像素大小。它只需将面板添加到框架中,然后调用 pack。
paintComponent 方法是所有自定义绘制发生的地方。这个方法由 javax.swing.JComponent 定义,然后被你的子类重写以提供它们的自定义行为。它唯一的参数,一个 java.awt.Graphics 对象,提供了许多用于绘制 2D 形状和获取有关应用程序图形环境信息的方法。在大多数情况下,实际接收到这个方法的对象将是 java.awt.Graphics2D 的一个实例(Graphics 的子类),它提供了对复杂的 2D 图形渲染的支持。
大多数标准的 Swing 组件都通过单独的"UI Delegate"对象实现其外观和感觉。调用super.paintComponent(g)将图形上下文传递给组件的 UI 代理,后者绘制面板的背景。要更仔细地了解这个过程,请参见上述 SDN 文章中标题为"绘制和 UI 代理"的部分。
练习:
-
现在你已经向屏幕绘制了一些自定义文本,请尝试像之前那样最小化和恢复应用程序。
-
用另一个窗口遮挡文本的一部分,然后将该窗口移开以重新显示自定义文本。在这两种情况下,绘制子系统将确定组件已损坏,并确保调用你的
paintComponent方法。
创建演示应用程序(第 3 步)
原文:
docs.oracle.com/javase/tutorial/uiswing/painting/step3.html
最后,我们将添加事件处理代码,以在用户单击或拖动鼠标时以编程方式重新绘制组件。为了尽可能高效地保持我们的自定义绘画,我们将跟踪鼠标坐标并仅重新绘制屏幕上发生变化的区域。这是一个推荐的最佳实践,将使您的应用程序尽可能高效地运行。

完成的应用程序
图 3:完成的应用程序
点击“启动”按钮以使用Java™ Web Start运行 SwingPaintDemo3(下载 JDK 7 或更高版本)。或者,要自行编译和运行示例,请参考示例索引。
package painting;
import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.BorderFactory;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseMotionAdapter;
public class SwingPaintDemo3 {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
}
private static void createAndShowGUI() {
System.out.println("Created GUI on EDT? "+
SwingUtilities.isEventDispatchThread());
JFrame f = new JFrame("Swing Paint Demo");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(new MyPanel());
f.pack();
f.setVisible(true);
}
}
class MyPanel extends JPanel {
private int squareX = 50;
private int squareY = 50;
private int squareW = 20;
private int squareH = 20;
public MyPanel() {
setBorder(BorderFactory.createLineBorder(Color.black));
addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
moveSquare(e.getX(),e.getY());
}
});
addMouseMotionListener(new MouseAdapter() {
public void mouseDragged(MouseEvent e) {
moveSquare(e.getX(),e.getY());
}
});
}
private void moveSquare(int x, int y) {
int OFFSET = 1;
if ((squareX!=x) || (squareY!=y)) {
repaint(squareX,squareY,squareW+OFFSET,squareH+OFFSET);
squareX=x;
squareY=y;
repaint(squareX,squareY,squareW+OFFSET,squareH+OFFSET);
}
}
public Dimension getPreferredSize() {
return new Dimension(250,200);
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawString("This is my custom Panel!",10,20);
g.setColor(Color.RED);
g.fillRect(squareX,squareY,squareW,squareH);
g.setColor(Color.BLACK);
g.drawRect(squareX,squareY,squareW,squareH);
}
}
这个变化首先从java.awt.event包中导入各种鼠标类,使应用程序能够响应用户的鼠标活动。构造函数已更新以注册鼠标按下和拖动的事件侦听器。每当接收到MouseEvent时,它会被转发到moveSquare方法,该方法会智能地更新正方形的坐标并重新绘制组件。请注意,默认情况下,放置在这些事件处理程序中的任何代码都将在事件分发线程上执行。
但最重要的变化是调用repaint方法。这个方法由java.awt.Component定义,是允许你以编程方式重新绘制给定组件表面的机制。它有一个无参版本(重新绘制整个组件)和一个多参数版本(重新绘制指定区域)。这个区域也被称为剪辑区。调用repaint的多参数版本需要额外的努力,但可以保证你的绘画代码不会浪费时间重新绘制屏幕上未改变的区域。
因为我们手动设置了剪辑区域,我们的moveSquare方法不是一次调用 repaint 方法,而是两次。第一次调用告诉 Swing 重新绘制组件中方块之前所在的区域(继承的行为使用 UI Delegate 来填充该区域的当前背景颜色)。第二次调用绘制组件中方块当前所在的区域。值得注意的重要一点是,尽管我们在同一个事件处理程序中连续两次调用 repaint,但 Swing 足够智能,可以将这些信息合并在一个单一的绘制操作中重新绘制屏幕的这些部分。换句话说,Swing 不会连续两次重绘组件,即使代码看起来是这样做的。
练习:
-
将第一次调用 repaint 注释掉,并注意当你点击或拖动鼠标时会发生什么。因为那一行负责填充背景,你应该注意到所有方块在被绘制后仍然保留在屏幕上。
-
在屏幕上有多个方块的情况下,最小化和恢复应用程序窗口。会发生什么?你应该注意到最大化屏幕会导致系统完全重绘组件表面,这将擦除除当前方块外的所有方块。
-
将两次调用 repaint 都注释掉,并在 paintComponent 方法的末尾添加一行调用 repaint 的零参数版本。应用程序将看起来恢复到其原始行为,但现在绘制效率会降低,因为整个组件的表面区域现在都在被绘制。你可能会注意到性能变慢,特别是如果应用程序被最大化时。
优化设计
原文:
docs.oracle.com/javase/tutorial/uiswing/painting/refining.html
为了演示目的,将绘图逻辑完全包含在MyPanel类中是有意义的。但是,如果您的应用程序需要跟踪多个实例,您可以使用一种模式,将该代码提取到一个单独的类中,以便每个正方形可以被视为一个独立的对象。这种技术在 2D 游戏编程中很常见,有时被称为“精灵动画”。
点击“启动”按钮以使用Java™ Web Start运行 SwingPaintDemo4(下载 JDK 7 或更高版本)。或者,要自行编译和运行示例,请参考示例索引。
package painting;
import javax.swing.SwingUtilities;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.BorderFactory;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseMotionAdapter;
public class SwingPaintDemo4 {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
}
private static void createAndShowGUI() {
System.out.println("Created GUI on EDT? "+
SwingUtilities.isEventDispatchThread());
JFrame f = new JFrame("Swing Paint Demo");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(new MyPanel());
f.setSize(250,250);
f.setVisible(true);
}
}
class MyPanel extends JPanel {
RedSquare redSquare = new RedSquare();
public MyPanel() {
setBorder(BorderFactory.createLineBorder(Color.black));
addMouseListener(new MouseAdapter(){
public void mousePressed(MouseEvent e){
moveSquare(e.getX(),e.getY());
}
});
addMouseMotionListener(new MouseAdapter(){
public void mouseDragged(MouseEvent e){
moveSquare(e.getX(),e.getY());
}
});
}
private void moveSquare(int x, int y){
// Current square state, stored as final variables
// to avoid repeat invocations of the same methods.
final int CURR_X = redSquare.getX();
final int CURR_Y = redSquare.getY();
final int CURR_W = redSquare.getWidth();
final int CURR_H = redSquare.getHeight();
final int OFFSET = 1;
if ((CURR_X!=x) || (CURR_Y!=y)) {
// The square is moving, repaint background
// over the old square location.
repaint(CURR_X,CURR_Y,CURR_W+OFFSET,CURR_H+OFFSET);
// Update coordinates.
redSquare.setX(x);
redSquare.setY(y);
// Repaint the square at the new location.
repaint(redSquare.getX(), redSquare.getY(),
redSquare.getWidth()+OFFSET,
redSquare.getHeight()+OFFSET);
}
}
public Dimension getPreferredSize() {
return new Dimension(250,200);
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawString("This is my custom Panel!",10,20);
redSquare.paintSquare(g);
}
}
class RedSquare{
private int xPos = 50;
private int yPos = 50;
private int width = 20;
private int height = 20;
public void setX(int xPos){
this.xPos = xPos;
}
public int getX(){
return xPos;
}
public void setY(int yPos){
this.yPos = yPos;
}
public int getY(){
return yPos;
}
public int getWidth(){
return width;
}
public int getHeight(){
return height;
}
public void paintSquare(Graphics g){
g.setColor(Color.RED);
g.fillRect(xPos,yPos,width,height);
g.setColor(Color.BLACK);
g.drawRect(xPos,yPos,width,height);
}
}
在这个特定的实现中,我们完全从头开始创建了一个RedSquare类。另一种方法是通过使RedSquare成为java.awt.Rectangle的子类来重用其功能。无论RedSquare如何实现,重要的是我们给该类一个接受Graphics对象的方法,并且该方法从面板的paintComponent方法中调用。这种分离使您的代码保持清晰,因为它基本上告诉每个红色正方形自己绘制。
细看绘制机制
原文:
docs.oracle.com/javase/tutorial/uiswing/painting/closer.html
现在您知道paintComponent方法是放置所有绘制代码的地方。虽然这个方法在绘制时会被调用,但实际上绘制是从类层次结构的更高层开始的,即paint方法(由java.awt.Component定义)。每当您的组件需要呈现时,绘制子系统都会执行此方法。其签名为:
public void paint(Graphics g)
javax.swing.JComponent扩展了这个类,并将paint方法进一步分解为三个单独的方法,按以下顺序调用:
-
protected void paintComponent(Graphics g) -
protected void paintBorder(Graphics g) -
protected void paintChildren(Graphics g)
API 并未阻止您的代码覆盖paintBorder和paintChildren,但一般来说,您没有理由这样做。在实际情况下,paintComponent将是您唯一需要覆盖的方法。
正如先前提到的,大多数标准的 Swing 组件都通过单独的 UI 代理实现其外观和感觉。这意味着大多数(或全部)标准 Swing 组件的绘制过程如下进行。
-
paint()调用paintComponent(). -
如果
ui属性非空,则paintComponent()会调用ui.update()。 -
如果组件的
opaque属性为 true,则ui.update()会用背景颜色填充组件的背景并调用ui.paint()。 -
ui.paint()渲染组件的内容。
这就是为什么我们的SwingPaintDemo代码调用super.paintComponent(g)。我们可以添加额外的注释以使其更清晰:
public void paintComponent(Graphics g) { // Let UI Delegate paint first, which
// includes background filling since
// this component is opaque.
super.paintComponent(g);
g.drawString("This is my custom Panel!",10,20);
redSquare.paintSquare(g);
}
如果您已经理解了本课程提供的所有演示代码,恭喜!您已经具备足够的实际知识来在自己的应用程序中编写高效的绘制代码。但是,如果您想更深入地了解“底层原理”,请参考本课程第一页链接的 SDN 文章。
概要
原文:
docs.oracle.com/javase/tutorial/uiswing/painting/summary.html
-
在 Swing 中,绘制始于
paint方法,然后调用paintComponent、paintBorder和paintChildren。当组件首次绘制、调整大小或在被另一个窗口隐藏后再次暴露时,系统将自动调用这些方法。 -
通过调用组件的
repaint方法来实现程序化重绘;不要直接调用其paintComponent。调用repaint会导致绘制子系统采取必要步骤,以确保您的paintComponent方法在适当的时间被调用。 -
repaint的多参数版本允许您缩小组件的剪辑矩形(受绘制操作影响的屏幕部分),以使绘制更有效。我们在moveSquare方法中利用了这种技术,以避免重新绘制未发生变化的屏幕部分。还有一个无参数版本的方法,它将重新绘制组件的整个表面区域。 -
因为我们已经缩小了剪辑矩形,我们的
moveSquare方法不止一次地调用repaint。第一次调用重新绘制了组件中方块先前所在的区域(继承的行为是用当前背景颜色填充区域)。第二次调用绘制了组件中方块当前所在的区域。 -
您可以在同一事件处理程序中多次调用
repaint,但 Swing 将获取这些信息并在一次操作中重新绘制组件。 -
对于具有 UI Delegate 的组件,您应该将
Graphics参数与super.paintComponent(g)这一行代码作为您的paintComponent覆盖中的第一行代码传递。如果不这样做,那么您的组件将负责手动绘制其背景。您可以通过注释掉该行并重新编译来进行实验,看看背景不再被绘制。 -
通过将新代码提取到一个单独的
RedSquare类中,应用程序保持了面向对象的设计,这使得MyPanel类的paintComponent方法保持清晰。绘制仍然有效,因为我们通过调用其paintSquare(Graphics g)方法将Graphics对象传递给了红色方块。请记住,这个方法的名称是我们从头开始创建的;我们没有从 Swing API 的更高层次覆盖paintSquare。
解决常见绘制问题
原文:
docs.oracle.com/javase/tutorial/uiswing/painting/problems.html
问题: 我不知道在哪里放置我的绘制代码。
- 绘制代码应该放在任何继承自
JComponent的组件的paintComponent方法中。
问题: 我绘制的内容不显示。
-
检查您的组件是否完全显示。解决常见组件问题应该可以帮助您解决这个问题。
-
检查是否在需要更新外观时在组件上调用了
repaint。
问题: 我的组件的前景显示出来,但背景是不可见的。结果是,我的组件后面的一个或多个组件意外可见。
-
确保您的组件是不透明的。例如,许多外观和感觉中的
JPanel默认是不透明的。要使诸如JLabel和 GTK+JPanel之类的组件不透明,必须在它们上调用setOpaque(true)。 -
如果您的自定义组件扩展了
JPanel或更专业的JComponent后代,则可以通过在绘制组件内容之前调用super.paintComponent来绘制背景。 -
您可以使用以下代码在自定义组件的
paintComponent方法顶部自行绘制背景:g.setColor(getBackground()); g.fillRect(0, 0, getWidth(), getHeight()); g.setColor(getForeground());
问题: 我使用setBackground设置组件的背景颜色,但似乎没有效果。
- 很可能是因为您的组件没有绘制其背景,要么是因为它不是不透明的,要么是因为您的自定义绘制代码没有绘制背景。例如,如果您为
JLabel设置背景颜色,则还必须在标签上调用setOpaque(true)以使标签的背景被绘制。
问题: 我使用的代码与教程示例完全相同,但不起作用。为什么?
- 代码是否在与教程示例完全相同的方法中执行?例如,如果教程示例中的代码在示例的
paintComponent方法中,则该方法可能是代码保证正常工作的唯一位置。
问题: 如何绘制粗线?模式?
- Java™ 2D API 提供了广泛的支持,用于实现线宽和样式,以及用于填充和描边形状的模式。有关使用 Java 2D API 的更多信息,请参阅 2D 图形教程。
问题: 特定组件的边缘看起来很奇怪。
-
因为组件通常会更新其边框以反映组件状态,所以通常应避免在除了
JPanel和JComponent的自定义子类之外的组件上调用setBorder。 -
组件是否由类似 GTK+或 Windows XP 的外观和感觉绘制,这些外观和感觉使用 UI 绘制的边框而不是
Border对象?如果是这样,请不要在组件上调用setBorder。 -
组件是否有自定义绘制代码?如果有,绘制代码是否考虑了组件的插图?
问题: 我的 GUI 中出现视觉伪影。
-
如果设置组件的背景颜色,请确保颜色没有透明度,如果组件应该是不透明的。
-
如果需要,使用
setOpaque方法设置组件的不透明度。例如,内容窗格必须是不透明的,但具有透明背景的组件不得是不透明的。 -
确保你的自定义组件完全填充其绘制区域,如果它是不透明的。
问题: 我的自定义绘制代码性能较差。
-
如果你可以绘制组件的一部分,请使用
Graphics的getClip或getClipBounds方法确定需要绘制的区域。你绘制的越少,速度就越快。 -
如果只有组件的一部分需要更新,请使用指定绘制区域的
repaint版本发出绘制请求。 -
想要了解如何选择高效的绘制技术,请查看Java Media APIs home page中包含"performance"字符串的部分。
问题: 应用于看似相同的Graphics对象的相同变换有时会产生略有不同的效果。
- 因为 Swing 绘制代码在调用
paintComponent之前设置了变换(使用Graphics的translate方法),所以你应用的任何变换都会具有累积效果。这在进行简单平移时并不重要,但是一个更复杂的AffineTransform可能会产生意想不到的结果。
如果你在这个列表中找不到你的问题,请查看 Solving Common Component Problems 和 Solving Common Layout Problems。
问题和练习:执行自定义绘图
原文:
docs.oracle.com/javase/tutorial/uiswing/QandE/questions-ch6.html
问题
1. JComponent 定义的哪个方法绘制组件的内部?
2. 以下哪个代码片段绘制了一个大小为 100x100 像素的矩形(填充或不填充)?
a. g.fillRect(x, y, 100, 100)
b. g.fillRect(x, y, 99, 99)
c. g.drawRect(x, y, 100, 100)
d. b 和 c
e. a 和 c
3. 你会使用什么代码使组件使用背景颜色的 50%透明度执行下一个绘图操作?
练习
1. 使用标准边框和自定义组件绘制,实现一个首选大小为 250x100 的组件,默认情况下是不透明的,有一个 5 像素的黑色边框,并在前景色中绘制一个“X”(使用 5 像素粗线),如下图所示。

2. 实现一个图标,大小为 10x10 像素,绘制填充整个 10x10 区域的实心矩形。如果图标的组件启用,则矩形应为红色;如果禁用,则为灰色。制作一个使用你自定义的Icon替换中间按钮显示middle.gif的ButtonDemo.java的副本。以下图片展示了图标应该是什么样子。
![]() |
![]() |
|---|
3. 实现一个边框,它在其组件顶部整个宽度处绘制一个红色的 15 像素高条纹。通过将此边框替换为练习 1 中创建的组件上的边框来测试此边框。结果应如下图所示。
检查你的答案。
Trail: 集合
本节描述了 Java 集合框架。在这里,您将了解什么是集合,以及它们如何使您的工作更轻松,程序更好。您将了解构成 Java 集合框架的核心元素——接口、实现、聚合操作和算法。
告诉您什么是集合,以及它们如何使您的工作更轻松,程序更好。您将了解构成集合框架的核心元素:接口、实现和算法。
描述了 Java 集合框架的核心集合接口,这些接口是 Java 集合框架的核心。您将学习如何有效使用这些接口的一般准则,包括何时使用哪个接口。您还将学习每个接口的习语,这将帮助您充分利用这些接口。
代表您迭代集合,使您能够编写更简洁和高效的代码来处理存储在集合中的元素。
描述了 JDK 的通用集合实现,并告诉您何时使用哪种实现。您还将了解包装器实现,它们为通用实现添加功能。
描述了 JDK 提供的用于操作集合的多态算法。幸运的话,您将永远不必再编写自己的排序程序了!
告诉您为什么您可能希望编写自己的集合实现(而不是使用 JDK 提供的通用实现之一),以及您应该如何去做。使用 JDK 的抽象集合实现非常容易!
告诉您 Java 集合框架如何与早于 Java 集合添加之前的旧 API 进行互操作。此外,它还告诉您如何设计新的 API,以便它们可以与其他新的 API 无缝互操作。
课程:集合简介
原文:
docs.oracle.com/javase/tutorial/collections/intro/index.html
一个集合 有时被称为容器 简单地是将多个元素组合成一个单元的对象。集合用于存储、检索、操作和传递聚合数据。通常,它们代表形成自然组的数据项,例如扑克手(一组卡片)、邮件文件夹(一组信件)或电话目录(名称到电话号码的映射)。如果您使用过 Java 编程语言 或几乎任何其他编程语言 您已经熟悉集合。
什么是集合框架?
集合框架是用于表示和操作集合的统一架构。所有集合框架都包含以下内容:
-
接口: 这些是表示集合的抽象数据类型。接口允许独立于其表示细节地操作集合。在面向对象的语言中,接口通常形成一个层次结构。
-
实现: 这些是集合接口的具体实现。实质上,它们是可重用的数据结构。
-
算法: 这些是执行有用计算的方法,例如在实现集合接口的对象上进行搜索和排序。这些算法被称为多态:也就是说,相同的方法可以用于适当集合接口的许多不同实现。实质上,算法是可重用的功能。
除了 Java 集合框架外,最著名的集合框架示例是 C++标准模板库(STL)和 Smalltalk 的集合层次结构。从历史上看,集合框架一直相当复杂,这使它们声名狼藉,被认为具有陡峭的学习曲线。我们相信 Java 集合框架打破了这一传统,您将在本章中亲自体会到。
Java 集合框架的好处
Java 集合框架提供以下好处:
-
减少编程工作量: 通过提供有用的数据结构和算法,集合框架使您能够集中精力于程序的重要部分,而不是在使其工作所需的低级“管道”上。通过促进不相关 API 之间的互操作性,Java 集合框架使您无需编写适配器对象或转换代码来连接 API。
-
提高程序速度和质量: 这个集合框架提供了高性能、高质量的有用数据结构和算法实现。每个接口的各种实现是可互换的,因此通过切换集合实现,程序可以轻松调整。因为你摆脱了编写自己数据结构的苦差事,你将有更多时间用于提高程序的质量和性能。
-
允许不相关 API 之间的互操作性: 集合接口是 API 之间传递集合的通用方式。如果我的网络管理 API 提供了一个节点名称的集合,而你的 GUI 工具包期望一个列标题的集合,我们的 API 将能够无缝地互操作,即使它们是独立编写的。
-
减少学习和使用新 API 的工作量: 许多 API 自然地接受集合作为输入并将其作为输出提供。过去,每个这样的 API 都有一个专门用于操作其集合的小子 API。这些临时集合子 API 之间几乎没有一致性,因此您必须从头开始学习每一个,并且在使用它们时很容易出错。随着标准集合接口的出现,这个问题消失了。
-
减少设计新 API 的工作量: 这是前一优势的另一面。设计者和实施者在创建依赖于集合的 API 时不必每次都重新发明轮子;相反,他们可以使用标准的集合接口。
-
促进软件重用: 符合标准集合接口的新数据结构本质上是可重用的。对实现这些接口的对象进行操作的新算法也是如此。
课程:接口
原文:
docs.oracle.com/javase/tutorial/collections/interfaces/index.html
核心集合接口封装了不同类型的集合,如下图所示。这些接口允许独立于其表示细节来操作集合。核心集合接口是 Java 集合框架的基础。如下图所示,核心集合接口形成了一个层次结构。

核心集合接口。
Set 是一种特殊类型的 Collection,SortedSet 是一种特殊类型的 Set,依此类推。还要注意,层次结构由两个不同的树组成 Map 不是真正的 Collection。
请注意,所有核心集合接口都是泛型的。例如,这是 Collection 接口的声明。
public interface Collection<E>...
<E> 语法告诉你该接口是泛型的。当你声明一个 Collection 实例时,你可以并且应该指定集合中包含的对象类型。指定类型允许编译器在编译时验证你放入集合中的对象类型是否正确,从而减少运行时错误。有关泛型类型的信息,请参阅泛型(已更新)课程。
当你了解如何使用这些接口时,你将了解大部分关于 Java 集合框架的知识。本章讨论了有效使用接口的一般准则,包括何时使用哪个接口。您还将学习每个接口的编程习惯用法,以帮助您充分利用它。
为了使核心集合接口的数量可管理,Java 平台没有为每种集合类型的每种变体提供单独的接口。(这些变体可能包括不可变、固定大小和仅追加的。)相反,每个接口中的修改操作被指定为可选的 给定的实现可以选择不支持所有操作。如果调用了不受支持的操作,集合会抛出一个UnsupportedOperationException。实现负责记录支持哪些可选操作。Java 平台的通用实现支持所有可选操作。
以下列表描述了核心集合接口:
-
Collection 集合层次结构的根。一个集合代表一组称为其元素的对象。Collection接口是所有集合实现的最小公分母,用于在需要最大一般性时传递集合并操作它们。某些类型的集合允许重复元素,而其他类型则不允许。有些是有序的,而其他是无序的。Java 平台没有直接提供这个接口的任何实现,但提供了更具体的子接口的实现,比如Set和List。另请参阅集合接口部分。 -
Set 一个不能包含重复元素的集合。这个接口模拟了数学集合抽象,并用于表示集合,比如组成扑克手的牌,组成学生课程表的课程,或者机器上运行的进程。另请参阅集合接口部分。 -
List 一个有序集合(有时被称为序列)。List可以包含重复元素。List的用户通常可以精确控制每个元素在列表中的插入位置,并且可以通过它们的整数索引(位置)访问元素。如果你使用过Vector,那么你对List的一般特性已经很熟悉。另请参阅列表接口部分。 -
Queue 用于在处理之前保存多个元素的集合。除了基本的Collection操作外,Queue还提供额外的插入、提取和检查操作。队列通常按照 FIFO(先进先出)的方式对元素进行排序,但并非必须如此。优先队列是其中的例外,它根据提供的比较器或元素的自然顺序对元素进行排序。无论使用何种排序方式,队列的头部都是通过调用
remove或poll将被移除的元素。在 FIFO 队列中,所有新元素都插入到队列的尾部。其他类型的队列可能使用不同的放置规则。每个Queue实现必须指定其排序属性。另请参阅队列接口部分。 -
Deque 用于在处理之前保存多个元素的集合。除了基本的Collection操作外,Deque还提供额外的插入、提取和检查操作。双端队列可以同时作为 FIFO(先进先出)和 LIFO(后进先出)使用。在双端队列中,所有新元素都可以在两端插入、检索和移除。另请参阅双端队列接口部分。
-
Map 一个将键映射到值的对象。Map不能包含重复键;每个键最多可以映射到一个值。如果你使用过Hashtable,那么你已经熟悉Map的基础知识。另请参阅映射接口部分。
最后两个核心集合接口仅仅是Set和Map的排序版本:
-
SortedSet 一个按升序维护其元素的Set。提供了几个额外的操作来利用排序。排序集用于自然排序的集合,比如单词列表和成员名单。另请参阅 SortedSet 接口部分。 -
SortedMap 一个按升序维护其映射的Map。这是SortedSet的Map类比。排序映射用于自然排序的键/值对集合,比如字典和电话目录。另请参阅 SortedMap 接口部分。
要了解排序接口如何维护其元素的顺序,请参阅对象排序部分。
集合接口
原文:
docs.oracle.com/javase/tutorial/collections/interfaces/collection.html
一个Collection代表一组被称为元素的对象。Collection接口用于传递希望具有最大一般性的对象集合。例如,按照惯例,所有通用集合实现都有一个接受Collection参数的构造函数。这个构造函数,称为转换构造函数,初始化新集合以包含指定集合中的所有元素,无论给定集合的子接口或实现类型如何。换句话说,它允许你转换集合的类型。
例如,假设您有一个Collection<String> c,它可以是List,Set或另一种类型的Collection。这种习惯用法创建一个新的ArrayList(List接口的实现),最初包含c中的所有元素。
List<String> list = new ArrayList<String>(c);
或者 - 如果您使用的是 JDK 7 或更高版本 - 您可以使用菱形操作符:
List<String> list = new ArrayList<>(c);
Collection接口包含执行基本操作的方法,如int size(),boolean isEmpty(),boolean contains(Object element),boolean add(E element),boolean remove(Object element)和Iterator<E> iterator()。
它还包含对整个集合进行操作的方法,如boolean containsAll(Collection<?> c),boolean addAll(Collection<? extends E> c),boolean removeAll(Collection<?> c),boolean retainAll(Collection<?> c)和void clear()。
还存在用于数组操作的其他方法(如Object[] toArray()和<T> T[] toArray(T[] a))。
在 JDK 8 及更高版本中,Collection接口还公开了方法Stream<E> stream()和Stream<E> parallelStream(),用于从底层集合获取顺序或并行流。 (有关使用流的更多信息,请参见名为聚合操作的课程。)
Collection接口做了你所期望的事情,因为Collection代表一组对象。它有告诉你集合中有多少元素的方法(size,isEmpty),检查给定对象是否在集合中的方法(contains),向集合中添加和删除元素的方法(add,remove),以及提供集合迭代器的方法(iterator)。
add方法定义得足够通用,以便对允许重复的集合和不允许重复的集合都有意义。它保证在调用完成后Collection将包含指定的元素,并且如果调用导致Collection发生更改,则返回true。类似地,remove方法旨在从Collection中删除指定元素的单个实例,假设它起初包含该元素,并在结果中修改Collection时返回true。
遍历集合
有三种遍历集合的方式:(1) 使用聚合操作 (2) 使用for-each结构和 (3) 使用Iterator。
聚合操作
在 JDK 8 及更高版本中,迭代集合的首选方法是获取流并对其执行聚合操作。聚合操作通常与 lambda 表达式结合使用,使编程更具表现力,代码行数更少。以下代码顺序迭代形状集合并打印出红色对象:
myShapesCollection.stream()
.filter(e -> e.getColor() == Color.RED)
.forEach(e -> System.out.println(e.getName()));
同样,您可以轻松请求并行流,如果集合足够大且计算机具有足够的核心,则可能是有意义的:
myShapesCollection.parallelStream()
.filter(e -> e.getColor() == Color.RED)
.forEach(e -> System.out.println(e.getName()));
使用此 API 收集数据有许多不同的方法。例如,您可能希望将Collection的元素转换为String对象,然后以逗号分隔它们:
String joined = elements.stream()
.map(Object::toString)
.collect(Collectors.joining(", "));
或者计算所有员工的工资总和:
int total = employees.stream()
.collect(Collectors.summingInt(Employee::getSalary)));
这些只是使用流和聚合操作可以做的一些示例。有关更多信息和示例,请参阅名为 Aggregate Operations 的课程。
集合框架始终作为其 API 的一部分提供了许多所谓的“批量操作”。这些包括操作整个集合的方法,如containsAll、addAll、removeAll等。不要将这些方法与 JDK 8 中引入的聚合操作混淆。新聚合操作和现有的批量操作(containsAll、addAll等)之间的关键区别在于旧版本都是变异的,意味着它们都修改基础集合。相反,新的聚合操作不修改基础集合。在使用新的聚合操作和 lambda 表达式时,您必须小心避免突变,以免在将来从并行流运行代码时引入问题。
for-each 结构
for-each结构允许您简洁地使用for循环遍历集合或数组 请参阅 for 语句。以下代码使用for-each结构将集合的每个元素打印在单独的行上。
for (Object o : collection)
System.out.println(o);
迭代器
一个Iterator是一个对象,它使您能够遍历集合并根据需要选择性地删除集合中的元素。通过调用其iterator方法,您可以为集合获取一个Iterator。以下是Iterator接口。
public interface Iterator<E> {
boolean hasNext();
E next();
void remove(); //optional
}
hasNext方法在迭代还有更多元素时返回true,next方法返回迭代中的下一个元素。remove方法从底层Collection中删除上次由next返回的最后一个元素。remove方法每次调用next只能调用一次,如果违反此规则会抛出异常。
请注意,Iterator.remove是在迭代期间修改集合的唯一安全方式;如果在迭代进行时以任何其他方式修改底层集合,则行为是未指定的。
当您需要时,请使用Iterator而不是for-each结构:
-
删除当前元素。
for-each结构隐藏了迭代器,因此无法调用remove。因此,for-each结构不适用于过滤。 -
并行迭代多个集合。
以下方法向您展示如何使用Iterator来过滤任意的Collection 也就是遍历集合并删除特定元素。
static void filter(Collection<?> c) {
for (Iterator<?> it = c.iterator(); it.hasNext(); )
if (!cond(it.next()))
it.remove();
}
这段简单的代码是多态的,这意味着它适用于任何Collection,无论实现方式如何。此示例演示了使用 Java 集合框架编写多态算法有多么容易。
集合接口批量操作
批量操作在整个Collection上执行操作。您可以使用基本操作实现这些简写操作,尽管在大多数情况下,这样的实现效率会更低。以下是批量操作:
-
containsAll 如果目标Collection包含指定Collection中的所有元素,则返回true。 -
addAll 将指定Collection中的所有元素添加到目标Collection中。 -
removeAll 从目标Collection中删除所有也包含在指定Collection中的元素。 -
retainAll 从目标Collection中删除所有不包含在指定Collection中的元素。也就是说,它仅保留目标Collection中也包含在指定Collection中的那些元素。 -
clear 从Collection中删除所有元素。
addAll、removeAll和retainAll方法在执行操作过程中修改了目标Collection时都会返回true。
作为批量操作强大性的一个简单示例,考虑以下惯用法来从Collection中删除所有指定元素e的所有实例,c。
c.removeAll(Collections.singleton(e));
更具体地说,假设您想要从Collection中删除所有null元素。
c.removeAll(Collections.singleton(null));
这种习惯用法使用 Collections.singleton,这是一个静态工厂方法,返回一个只包含指定元素的不可变 Set。
集合接口数组操作
toArray 方法作为集合和旧 API 之间的桥梁提供,这些旧 API 期望输入数组。数组操作允许将 Collection 的内容转换为数组。没有参数的简单形式创建一个新的 Object 数组。更复杂的形式允许调用者提供一个数组或选择输出数组的运行时类型。
例如,假设 c 是一个 Collection。以下代码段将 c 的内容转储到一个新分配的 Object 数组中,其长度与 c 中的元素数量相同。
Object[] a = c.toArray();
假设 c 只包含字符串(也许是因为 c 的类型是 Collection<String>)。以下代码段将 c 的内容转储到一个新分配的 String 数组中,其长度与 c 中的元素数量相同。
String[] a = c.toArray(new String[0]);
集合接口
原文:
docs.oracle.com/javase/tutorial/collections/interfaces/set.html
一个Set是一个Collection,不能包含重复元素。它模拟了数学集合的抽象。Set接口仅包含从Collection继承的方法,并添加了禁止重复元素的限制。Set还对equals和hashCode操作的行为添加了更强的约定,允许有意义地比较Set实例,即使它们的实现类型不同。如果两个Set实例包含相同的元素,则它们相等。
Java 平台包含三种通用的Set实现:HashSet、TreeSet和LinkedHashSet。HashSet将其元素存储在哈希表中,是性能最佳的实现;但是它不保证迭代顺序。TreeSet将其元素存储在红黑树中,根据值对其元素进行排序;它比HashSet慢得多。LinkedHashSet作为一个哈希表实现,其中有一个通过它的链表运行的链表,根据它们插入到集合中的顺序(插入顺序)对其元素进行排序。LinkedHashSet通过略微更高的成本使其客户免受HashSet提供的未指定、通常混乱的排序。
这是一个简单但有用的Set习语。假设你有一个Collection,c,你想创建另一个包含相同元素但消除所有重复项的Collection。以下一行代码就可以搞定。
Collection<Type> noDups = new HashSet<Type>(c);
它通过创建一个Set(根据定义,不能包含重复项),最初包含c中的所有元素。它使用在集合接口部分描述的标准转换构造函数。
或者,如果使用 JDK 8 或更高版本,您可以使用聚合操作轻松地收集到一个Set中:
c.stream()
.collect(Collectors.toSet()); // no duplicates
这是一个稍微更长的示例,将一个Collection中的名称累积到一个TreeSet中:
Set<String> set = people.stream()
.map(Person::getName)
.collect(Collectors.toCollection(TreeSet::new));
以下是第一个习语的一个次要变体,保留原始集合的顺序,同时删除重复元素:
Collection<Type> noDups = new LinkedHashSet<Type>(c);
以下是一个通用方法,封装了前面的习语,返回一个与传入的相同泛型类型的Set。
public static <E> Set<E> removeDups(Collection<E> c) {
return new LinkedHashSet<E>(c);
}
集合接口基本操作
size操作返回Set中元素的数量(其基数)。isEmpty方法执行您认为它会执行的操作。add方法将指定的元素添加到Set中(如果尚未存在)并返回一个指示元素是否已添加的布尔值。类似地,remove方法从Set中移除指定的元素(如果存在)并返回一个指示元素是否存在的布尔值。iterator方法返回Set上的Iterator。
以下程序打印出其参数列表中的所有不同单词。提供了两个版本的此程序。第一个使用了 JDK 8 聚合操作。第二个使用了for-each结构。
使用 JDK 8 聚合操作:
import java.util.*;
import java.util.stream.*;
public class FindDups {
public static void main(String[] args) {
Set<String> distinctWords = Arrays.asList(args).stream()
.collect(Collectors.toSet());
System.out.println(distinctWords.size()+
" distinct words: " +
distinctWords);
}
}
使用for-each结构:
import java.util.*;
public class FindDups {
public static void main(String[] args) {
Set<String> s = new HashSet<String>();
for (String a : args)
s.add(a);
System.out.println(s.size() + " distinct words: " + s);
}
}
现在运行程序的任一版本。
java FindDups i came i saw i left
生成以下输出:
4 distinct words: [left, came, saw, i]
请注意,代码总是通过其接口类型(Set)而不是其实现类型来引用Collection。这是一种强烈推荐的编程实践,因为它使您可以通过仅更改构造函数来灵活地更改实现。如果用于存储集合的任何变量或用于传递集合的参数声明为Collection的实现类型而不是其接口类型,那么为了更改其实现类型,所有这些变量和参数都必须更改。
此外,并不能保证生成的程序将正常工作。如果程序使用原始实现类型中存在但新实现类型中不存在的任何非标准操作,则程序将失败。仅通过其接口引用集合可以防止您使用任何非标准操作。
前面示例中Set的实现类型是HashSet,它不保证Set中元素的顺序。如果希望程序按字母顺序打印单词列表,只需将Set的实现类型从HashSet更改为TreeSet。进行这个微不足道的一行更改会导致前面示例中的命令行生成以下输出。
java FindDups i came i saw i left
4 distinct words: [came, i, left, saw]
设置接口批量操作
批量操作特别适用于Set;应用时,它们执行标准的集合代数操作。假设s1和s2是集合。以下是批量操作的作用:
-
s1.containsAll(s2) 如果s2是S1的子集,则返回true。(如果集合s1包含S2中的所有元素,则s2是s1的子集。) -
s1.addAll(s2) 将s1转换为s1和s2的并集。(两个集合的并集是包含在任一集合中的所有元素的集合。) -
s1.retainAll(s2) 将s1转换为s1和s2的交集。(两个集合的交集是仅包含两个集合共有元素的集合。) -
s1.removeAll(s2) 将s1转换为s1和s2的(非对称的)差集。(例如,s1减去s2的差集是包含s1中所有元素但不在s2中的元素的集合。)
要以非破坏性(不修改任何集合)计算两个集合的并集、交集或差集,调用者必须在调用适当的批量操作之前复制一个集合。以下是生成的习语。
Set<Type> union = new HashSet<Type>(s1);
union.addAll(s2);
Set<Type> intersection = new HashSet<Type>(s1);
intersection.retainAll(s2);
Set<Type> difference = new HashSet<Type>(s1);
difference.removeAll(s2);
在前述习语中,结果Set的实现类型是HashSet,正如前面提到的,这是 Java 平台上最好的通用Set实现。然而,任何通用的Set实现都可以替代。
让我们重新审视FindDups程序。假设你想知道参数列表中哪些单词只出现一次,哪些出现多次,但你不想重复打印任何重复的单词。这个效果可以通过生成两个集合来实现 一个包含参数列表中的每个单词,另一个只包含重复的单词。只出现一次的单词是这两个集合的差集,我们知道如何计算。生成的程序如下所示。
import java.util.*;
public class FindDups2 {
public static void main(String[] args) {
Set<String> uniques = new HashSet<String>();
Set<String> dups = new HashSet<String>();
for (String a : args)
if (!uniques.add(a))
dups.add(a);
// Destructive set-difference
uniques.removeAll(dups);
System.out.println("Unique words: " + uniques);
System.out.println("Duplicate words: " + dups);
}
}
当使用先前使用的相同参数列表运行时(i came i saw i left),程序产生以下输出。
Unique words: [left, saw, came]
Duplicate words: [i]
一个不太常见的集合代数操作是对称差集 包含在两个指定集合中的元素,但不在两者中都包含的元素的集合。以下代码以非破坏性方式计算两个集合的对称差集。
Set<Type> symmetricDiff = new HashSet<Type>(s1);
symmetricDiff.addAll(s2);
Set<Type> tmp = new HashSet<Type>(s1);
tmp.retainAll(s2);
symmetricDiff.removeAll(tmp);
集合接口数组操作
数组操作对Set并没有特殊处理,超出了它们对任何其他Collection的处理。这些操作在集合接口部分有描述。
列表接口
原文:
docs.oracle.com/javase/tutorial/collections/interfaces/list.html
一个List是一个有序的Collection(有时称为序列)。列表可以包含重复元素。除了从Collection继承的操作外,List接口还包括以下操作:
-
Positional access 根据列表中元素的数字位置来操作元素。这包括诸如get、set、add、addAll和remove等方法。 -
Search 在列表中搜索指定对象并返回其数字位置。搜索方法包括indexOf和lastIndexOf。 -
Iteration 扩展Iterator语义以利用列表的顺序性质。listIterator方法提供了这种行为。 -
Range-viewsublist方法在列表上执行任意范围操作。
Java 平台包含两个通用的List实现。ArrayList,通常是性能更好的实现,以及LinkedList,在某些情况下提供更好的性能。
集合操作
从Collection继承的操作都会做你期望的事情,假设你已经熟悉它们。如果你对Collection中的操作不熟悉,现在是阅读集合接口部分的好时机。remove操作总是从列表中移除第一个指定元素的出现。add和addAll操作总是将新元素追加到列表的末尾。因此,以下习语将一个列表连接到另一个列表。
list1.addAll(list2);
这是这个习语的非破坏性形式,它产生一个由第二个列表附加到第一个列表的第三个List。
List<Type> list3 = new ArrayList<Type>(list1);
list3.addAll(list2);
请注意,这个习语在其非破坏性形式中利用了ArrayList的标准转换构造函数。
这里有一个示例(JDK 8 及更高版本),将一些名称聚合到一个List中:
List<String> list = people.stream()
.map(Person::getName)
.collect(Collectors.toList());
像Set接口一样,List加强了对equals和hashCode方法的要求,以便可以比较两个List对象的逻辑相等性,而不考虑它们的实现类。如果两个List对象包含相同顺序的相同元素,则它们相等。
位置访问和搜索操作
基本的positional access操作是get、set、add和remove。(set和remove操作返回被覆盖或移除的旧值。)其他操作(indexOf和lastIndexOf)返回列表中指定元素的第一个或最后一个索引。
addAll操作在指定位置插入指定Collection的所有元素。元素按照指定Collection的迭代器返回的顺序插入。这个调用是Collection的addAll操作的位置访问模拟。
这是一个在List中交换两个索引值的小方法。
public static <E> void swap(List<E> a, int i, int j) {
E tmp = a.get(i);
a.set(i, a.get(j));
a.set(j, tmp);
}
当然,有一个很大的区别。这是一个多态算法:它在任何List中交换两个元素,而不管其实现类型如何。这是另一个使用前面swap方法的多态算法。
public static void shuffle(List<?> list, Random rnd) {
for (int i = list.size(); i > 1; i--)
swap(list, i - 1, rnd.nextInt(i));
}
这个算法包含在 Java 平台的Collections类中,使用指定的随机源随机排列指定的列表。这有点微妙:它从底部向上运行列表,重复地将随机选择的元素交换到当前位置。与大多数天真的洗牌尝试不同,它是公平的(假设随机源是无偏的,所有排列发生的概率相等),并且快速(只需要list.size()-1次交换)。以下程序使用这个算法以随机顺序打印其参数列表中的单词。
import java.util.*;
public class Shuffle {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
for (String a : args)
list.add(a);
Collections.shuffle(list, new Random());
System.out.println(list);
}
}
实际上,这个程序甚至可以变得更短更快。Arrays类有一个名为asList的静态工厂方法,允许将数组视为List。这个方法不会复制数组。对List的更改会写入数组,反之亦然。由于结果列表不是通用的List实现,因为它不实现(可选的)add和remove操作:数组不可调整大小。利用Arrays.asList并调用库版本的shuffle,它使用默认的随机源,你会得到以下小程序,其行为与之前的程序相同。
import java.util.*;
public class Shuffle {
public static void main(String[] args) {
List<String> list = Arrays.asList(args);
Collections.shuffle(list);
System.out.println(list);
}
}
迭代器
正如你所期望的,由List的iterator操作返回的Iterator以正确的顺序返回列表的元素。List还提供了一个更丰富的迭代器,称为ListIterator,允许你在任一方向遍历列表,在迭代期间修改列表,并获取迭代器的当前位置。
ListIterator从Iterator继承的三个方法(hasNext、next和remove)在两个接口中完全相同。hasPrevious和previous操作是hasNext和next的确切类比。前者操作指的是(隐式)光标之前的元素,而后者指的是光标之后的元素。previous操作将光标向后移动,而next将其向前移动。
这是通过列表向后迭代的标准习语。
for (ListIterator<Type> it = list.listIterator(list.size()); it.hasPrevious(); ) {
Type t = it.previous();
...
}
注意前面习语中listIterator的参数。List接口有两种形式的listIterator方法。没有参数的形式返回一个位于列表开头的ListIterator;带有一个int参数的形式返回一个位于指定索引处的ListIterator。索引指的是初始调用next时将返回的元素。初始调用previous将返回索引为index-1的元素。在长度为n的列表中,index有n+1个有效值,从0到n,包括0。
直观地说,光标始终位于两个元素之间 一个是调用previous返回的元素,另一个是调用next返回的元素。n+1个有效的index值对应于n+1个元素之间的间隙,从第一个元素之前的间隙到最后一个元素之后的间隙。以下图显示了包含四个元素的列表中的五个可能的光标位置。

五个可能的光标位置。
可以交替调用next和previous,但你必须小心。第一次调用previous返回的元素与最后一次调用next返回的元素相同。类似地,一系列调用previous后第一次调用next返回的元素与最后一次调用previous返回的元素相同。
nextIndex方法返回的是下一次调用next时将返回的元素的索引,而previousIndex返回的是下一次调用previous时将返回的元素的索引。这些调用通常用于报告找到某物的位置或记录ListIterator的位置,以便可以创建另一个具有相同位置的ListIterator。
nextIndex返回的数字始终比previousIndex返回的数字大1,这意味着两个边界情况的行为:(1) 当光标位于初始元素之前时,调用previousIndex返回-1,(2) 当光标位于最后一个元素之后时,调用nextIndex返回list.size()。为了使所有这些具体化,以下是List.indexOf的可能实现。
public int indexOf(E e) {
for (ListIterator<E> it = listIterator(); it.hasNext(); )
if (e == null ? it.next() == null : e.equals(it.next()))
return it.previousIndex();
// Element not found
return -1;
}
注意,尽管indexOf方法在正向遍历列表,但返回it.previousIndex()。原因是it.nextIndex()将返回我们即将检查的元素的索引,而我们想返回我们刚刚检查的元素的索引。
Iterator 接口提供了 remove 操作,用于从 Collection 中移除 next 返回的最后一个元素。对于 ListIterator,此操作会移除 next 或 previous 返回的最后一个元素。ListIterator 接口提供了两个额外的操作来修改列表 set 和 add。set 方法用指定的元素覆盖 next 或 previous 返回的最后一个元素。以下多态算法使用 set 来替换所有指定值的出现。
public static <E> void replace(List<E> list, E val, E newVal) {
for (ListIterator<E> it = list.listIterator(); it.hasNext(); )
if (val == null ? it.next() == null : val.equals(it.next()))
it.set(newVal);
}
这个示例中唯一棘手的部分是 val 和 it.next 之间的相等测试。你需要特殊处理 val 值为 null,以防止 NullPointerException。
add 方法在当前光标位置之前立即插入一个新元素到列表中。以下多态算法示例演示了用指定列表中包含的值序列替换所有指定值的出现。
public static <E>
void replace(List<E> list, E val, List<? extends E> newVals) {
for (ListIterator<E> it = list.listIterator(); it.hasNext(); ){
if (val == null ? it.next() == null : val.equals(it.next())) {
it.remove();
for (E e : newVals)
it.add(e);
}
}
}
范围视图操作
range-view 操作,subList(int fromIndex, int toIndex),返回一个 List 视图,其索引范围从 fromIndex(包括)到 toIndex(不包括)。这个 半开区间 与典型的 for 循环相似。
for (int i = fromIndex; i < toIndex; i++) {
...
}
如术语 view 所示,返回的 List 是由调用 subList 的 List 支持的,因此前者的更改会反映在后者中。
这个方法消除了显式范围操作的需要(通常对数组存在)。任何期望一个 List 的操作都可以通过传递一个 subList 视图而不是整个 List 来作为范围操作。例如,以下习语从一个 List 中移除一系列元素。
list.subList(fromIndex, toIndex).clear();
可以构建类似的习语来在一个范围内搜索元素。
int i = list.subList(fromIndex, toIndex).indexOf(o);
int j = list.subList(fromIndex, toIndex).lastIndexOf(o);
请注意,前述习语返回的是在 subList 中找到元素的索引,而不是在支持的 List 中的索引。
任何在 List 上操作的多态算法,比如 replace 和 shuffle 示例,都适用于 subList 返回的 List。
这是一个多态算法,其实现使用 subList 从一副牌中发牌。也就是说,它返回一个包含指定数量元素的新 List("手牌"),这些元素取自指定 List("牌堆")的末尾。手牌中返回的元素会从牌堆中移除。
public static <E> List<E> dealHand(List<E> deck, int n) {
int deckSize = deck.size();
List<E> handView = deck.subList(deckSize - n, deckSize);
List<E> hand = new ArrayList<E>(handView);
handView.clear();
return hand;
}
请注意,该算法从牌堆的 末尾 移除手牌。对于许多常见的 List 实现,比如 ArrayList,从列表末尾移除元素的性能要比从列表开头移除元素的性能好得多。
以下是一个使用 dealHand 方法结合 Collections.shuffle 从一副普通的 52 张牌的牌堆中生成手牌的 程序。该程序接受两个命令行参数:(1)要发的手牌数和(2)每手牌的牌数。
import java.util.*;
public class Deal {
public static void main(String[] args) {
if (args.length < 2) {
System.out.println("Usage: Deal hands cards");
return;
}
int numHands = Integer.parseInt(args[0]);
int cardsPerHand = Integer.parseInt(args[1]);
// Make a normal 52-card deck.
String[] suit = new String[] {
"spades", "hearts",
"diamonds", "clubs"
};
String[] rank = new String[] {
"ace", "2", "3", "4",
"5", "6", "7", "8", "9", "10",
"jack", "queen", "king"
};
List<String> deck = new ArrayList<String>();
for (int i = 0; i < suit.length; i++)
for (int j = 0; j < rank.length; j++)
deck.add(rank[j] + " of " + suit[i]);
// Shuffle the deck.
Collections.shuffle(deck);
if (numHands * cardsPerHand > deck.size()) {
System.out.println("Not enough cards.");
return;
}
for (int i = 0; i < numHands; i++)
System.out.println(dealHand(deck, cardsPerHand));
}
public static <E> List<E> dealHand(List<E> deck, int n) {
int deckSize = deck.size();
List<E> handView = deck.subList(deckSize - n, deckSize);
List<E> hand = new ArrayList<E>(handView);
handView.clear();
return hand;
}
}
运行该程序会产生如下输出。
% java Deal 4 5
[8 of hearts, jack of spades, 3 of spades, 4 of spades,
king of diamonds]
[4 of diamonds, ace of clubs, 6 of clubs, jack of hearts,
queen of hearts]
[7 of spades, 5 of spades, 2 of diamonds, queen of diamonds,
9 of clubs]
[8 of spades, 6 of diamonds, ace of spades, 3 of hearts,
ace of hearts]
虽然subList操作非常强大,但在使用时必须小心。如果在任何方式上除了通过返回的List之外对支持List进行添加或删除元素,那么subList返回的List的语义将变得不确定。因此,强烈建议您仅将subList返回的List用作临时对象,用于在支持List上执行一个或一系列范围操作。您使用subList实例的时间越长,您通过直接修改支持List或通过另一个subList对象来破坏它的可能性就越大。请注意,修改子列表的子列表并继续使用原始子列表(尽管不是同时)是合法的。
列表算法
Collections类中的大多数多态算法专门适用于List。拥有所有这些算法使得操作列表变得非常容易。以下是这些算法的摘要,在 Algorithms 部分中有更详细的描述。
-
sort 使用归并排序算法对List进行排序,这提供了一种快速、稳定的排序。(稳定排序是指不重新排列相等元素的排序。) -
shuffle 随机排列List中的元素。 -
reverse 颠倒List中元素的顺序。 -
rotate 将List中的所有元素按指定距离旋转。 -
swap 交换List中指定位置的元素。 -
replaceAll 用另一个指定值替换所有出现的值。 -
fill 用指定值覆盖List中的每个元素。 -
copy 将源List复制到目标List中。 -
binarySearch 使用二分搜索算法在有序List中搜索元素。 -
indexOfSubList 返回一个List中与另一个相等的第一个子列表的索引。 -
lastIndexOfSubList 返回一个List中与另一个相等的最后一个子列表的索引。
队列接口
原文:
docs.oracle.com/javase/tutorial/collections/interfaces/queue.html
一个Queue是在处理之前保存元素的集合。除了基本的 Collection 操作外,队列还提供额外的插入、移除和检查操作。Queue 接口如下。
public interface Queue<E> extends Collection<E> {
E element();
boolean offer(E e);
E peek();
E poll();
E remove();
}
每个 Queue 方法都有两种形式:(1)如果操作失败,则抛出异常,(2)如果操作失败,则返回特殊值(根据操作的不同,可能是 null 或 false)。接口的常规结构如下表所示。
队列接口结构
| 操作类型 | 抛出异常 | 返回特殊值 |
|---|---|---|
| 插入 | add(e) |
offer(e) |
| 移除 | remove() |
poll() |
| 检查 | element() |
peek() |
队列通常按照 FIFO(先进先出)的方式排序元素,但不一定如此。其中的例外是优先队列,它根据元素的值排序 —— 详见对象排序部分)。无论使用何种排序方式,队列的头部元素都是通过调用 remove 或 poll 方法将被移除的元素。在 FIFO 队列中,所有新元素都插入到队列的尾部。其他类型的队列可能使用不同的放置规则。每个 Queue 实现必须指定其排序属性。
一个 Queue 实现可能限制其持有的元素数量;这样的队列称为有界。java.util.concurrent 中的一些 Queue 实现是有界的,但 java.util 中的实现则不是。
add 方法是 Queue 从 Collection 继承的方法,除非违反队列的容量限制,否则会插入一个元素,否则会抛出 IllegalStateException 异常。offer 方法仅用于有界队列,与 add 的区别仅在于插入元素失败时返回 false。
remove 和 poll 方法都会移除并返回队列的头部。具体移除哪个元素取决于队列的排序策略。当队列为空时,remove 和 poll 方法的行为有所不同。在这种情况下,remove 会抛出 NoSuchElementException 异常,而 poll 则返回 null。
element 和 peek 方法会返回队列的头部元素,但不会将其移除。它们与 remove 和 poll 的区别与之相同:如果队列为空,element 会抛出 NoSuchElementException 异常,而 peek 则返回 null。
Queue实现通常不允许插入null元素。LinkedList实现是一个例外,它被改装为实现Queue。出于历史原因,它允许null元素,但你应该避免利用这一点,因为null被poll和peek方法用作特殊返回值。
队列实现通常不定义基于元素的equals和hashCode方法的版本,而是从Object继承基于身份的版本。
Queue接口不定义阻塞队列方法,这在并发编程中很常见。这些方法,等待元素出现或空间变得可用,被定义在接口java.util.concurrent.BlockingQueue中,它扩展了Queue。
在下面的示例程序中,使用队列实现倒计时器。队列预先加载了从命令行指定的数字到零的所有整数值,按降序排列。然后,这些值按一秒的间隔从队列中移除并打印。该程序是人为的,因为在不使用队列的情况下做同样的事情更自然,但它展示了使用队列在后续处理之前存储元素的用法。
import java.util.*;
public class Countdown {
public static void main(String[] args) throws InterruptedException {
int time = Integer.parseInt(args[0]);
Queue<Integer> queue = new LinkedList<Integer>();
for (int i = time; i >= 0; i--)
queue.add(i);
while (!queue.isEmpty()) {
System.out.println(queue.remove());
Thread.sleep(1000);
}
}
}
在下面的示例中,优先队列用于对一组元素进行排序。同样,这个程序是人为的,因为没有理由使用它来代替Collections提供的sort方法,但它展示了优先队列的行为。
static <E> List<E> heapSort(Collection<E> c) {
Queue<E> queue = new PriorityQueue<E>(c);
List<E> result = new ArrayList<E>();
while (!queue.isEmpty())
result.add(queue.remove());
return result;
}
Deque接口
原文:
docs.oracle.com/javase/tutorial/collections/interfaces/deque.html
通常发音为deck,双端队列是一种双端队列。双端队列是一种线性元素集合,支持在两个端点插入和移除元素。Deque接口比Stack和Queue更丰富的抽象数据类型,因为它同时实现了栈和队列。Deque接口定义了访问Deque实例两端元素的方法。提供了插入、移除和检查元素的方法。预定义类如ArrayDeque和LinkedList实现了Deque接口。
注意Deque接口可以同时用作后进先出栈和先进先出队列。Deque接口中的方法分为三部分:
插入
addFirst和offerFirst方法在Deque实例的开头插入元素。addLast和offerLast方法在Deque实例的末尾插入元素。当Deque实例的容量受限时,首选方法是offerFirst和offerLast,因为如果已满,则addFirst可能会失败而不抛出异常。
移除
removeFirst和pollFirst方法从Deque实例的开头移除元素。removeLast和pollLast方法从末尾移除元素。如果Deque为空,则pollFirst和pollLast方法返回null,而removeFirst和removeLast方法在Deque实例为空时会抛出异常。
检索
getFirst和peekFirst方法检索Deque实例的第一个元素。这些方法不会从Deque实例中移除值。类似地,getLast和peekLast方法检索最后一个元素。如果deque实例为空,则getFirst和getLast方法会抛出异常,而peekFirst和peekLast方法会返回NULL。
插入、移除和检索Deque元素的 12 种方法总结在以下表中:
Deque 方法
| 操作类型 | 第一个元素(Deque实例的开头) |
最后一个元素(Deque实例的末尾) |
|---|---|---|
| 插入 | addFirst(e) offerFirst(e) |
addLast(e) offerLast(e) |
| 移除 | removeFirst() pollFirst() |
removeLast() pollLast() |
| 检查 | getFirst() peekFirst() |
getLast() peekLast() |
除了这些基本方法用于插入、删除和检查Deque实例之外,Deque接口还有一些预定义方法。其中之一是removeFirstOccurence,此方法如果存在于Deque实例中,则删除指定元素的第一个出现。如果元素不存在,则Deque实例保持不变。另一个类似的方法是removeLastOccurence;此方法从Deque实例中删除指定元素的最后一个出现。这些方法的返回类型是boolean,如果元素存在于Deque实例中,则返回true。
Map 接口
原文:
docs.oracle.com/javase/tutorial/collections/interfaces/map.html
一个Map是一个将键映射到值的对象。地图不能包含重复的键:每个键最多可以映射到一个值。它模拟了数学函数抽象。Map接口包括基本操作的方法(如put,get,remove,containsKey,containsValue,size和empty),批量操作(如putAll和clear),以及集合视图(如keySet,entrySet和values)。
Java 平台包含三种通用的Map实现:HashMap,TreeMap,和LinkedHashMap。它们的行为和性能与HashSet,TreeSet和LinkedHashSet完全类似,如 Set 接口部分所述。
本页的其余部分详细讨论了Map接口。但首先,这里有一些使用 JDK 8 聚合操作收集到Map的更多示例。在面向对象编程中,对现实世界对象进行建模是一项常见任务,因此合理地认为一些程序可能会,例如,按部门分组员工:
// Group employees by department
Map<Department, List<Employee>> byDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
或者按部门计算所有工资的总和:
// Compute sum of salaries by department
Map<Department, Integer> totalByDept = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment,
Collectors.summingInt(Employee::getSalary)));
或者按及格或不及格分组学生:
// Partition students into passing and failing
Map<Boolean, List<Student>> passingFailing = students.stream()
.collect(Collectors.partitioningBy(s -> s.getGrade()>= PASS_THRESHOLD));
您还可以按城市分组人员:
// Classify Person objects by city
Map<String, List<Person>> peopleByCity
= personStream.collect(Collectors.groupingBy(Person::getCity));
或者甚至级联两个收集器,按州和城市对人员进行分类:
// Cascade Collectors
Map<String, Map<String, List<Person>>> peopleByStateAndCity
= personStream.collect(Collectors.groupingBy(Person::getState,
Collectors.groupingBy(Person::getCity)))
再次强调,这些只是如何使用新的 JDK 8 API 的一些示例。有关 lambda 表达式和聚合操作的深入覆盖,请参阅名为聚合操作的课程。
Map 接口基本操作
Map的基本操作(put,get,containsKey,containsValue,size和isEmpty)的行为与Hashtable中的对应操作完全相同。以下程序生成其参数列表中找到的单词的频率表。频率表将每个单词映射到其在参数列表中出现的次数。
import java.util.*;
public class Freq {
public static void main(String[] args) {
Map<String, Integer> m = new HashMap<String, Integer>();
// Initialize frequency table from command line
for (String a : args) {
Integer freq = m.get(a);
m.put(a, (freq == null) ? 1 : freq + 1);
}
System.out.println(m.size() + " distinct words:");
System.out.println(m);
}
}
关于此程序唯一棘手的事情是put语句的第二个参数。该参数是一个条件表达式,如果单词以前从未见过,则将频率设置为一,或者如果单词已经被看到,则将频率设置为当前值加一。尝试使用以下命令运行此程序:
java Freq if it is to be it is up to me to delegate
该程序产生以下输出。
8 distinct words:
{to=3, delegate=1, be=1, it=2, up=1, if=1, me=1, is=2}
假设您希望按字母顺序查看频率表。您只需将Map的实现类型从HashMap更改为TreeMap即可。通过进行这四个字符的更改,程序将从相同的命令行生成以下输出。
8 distinct words:
{be=1, delegate=1, if=1, is=2, it=2, me=1, to=3, up=1}
类似地,您可以通过将映射的实现类型更改为LinkedHashMap,使程序按照单词首次出现在命令行上的顺序打印频率表。这样做会产生以下输出。
8 distinct words:
{if=1, it=2, is=2, to=3, be=1, up=1, me=1, delegate=1}
这种灵活性提供了一个接口为基础的框架强大的例证。
像Set和List接口一样,Map加强了对equals和hashCode方法的要求,以便可以比较两个Map对象的逻辑相等性,而不考虑它们的实现类型。如果两个Map实例表示相同的键-值映射,则它们是相等的。
按照惯例,所有通用的Map实现都提供了接受Map对象并初始化新Map以包含指定Map中所有键-值映射的构造函数。这种标准的Map转换构造函数与标准的Collection构造函数完全类似:它允许调用者创建一个所需实现类型的Map,最初包含另一个Map中的所有映射,而不管另一个Map的实现类型如何。例如,假设您有一个名为m的Map。以下一行代码创建一个新的HashMap,最初包含与m相同的所有键-值映射。
Map<K, V> copy = new HashMap<K, V>(m);
Map 接口的批量操作
clear操作确切地做了你认为它能做的事情:它从Map中删除所有映射。putAll操作是Map接口的Collection接口的addAll操作的类比。除了明显的将一个Map倒入另一个Map的用途外,它还有第二个更微妙的用途。假设一个Map用于表示属性-值对的集合;putAll操作与Map转换构造函数结合使用,提供了一种实现带有默认值的属性映射创建的简洁方法。以下是演示这种技术的静态工厂方法。
static <K, V> Map<K, V> newAttributeMap(Map<K, V>defaults, Map<K, V> overrides) {
Map<K, V> result = new HashMap<K, V>(defaults);
result.putAll(overrides);
return result;
}
集合视图
Collection视图方法允许将Map以这三种方式视为Collection:
-
keySet 包含在Map中的键的Set。 -
values 包含在Map中的值的Collection。这个Collection不是Set,因为多个键可以映射到相同的值。 -
entrySet 包含在Map中的键-值对的Set。Map接口提供了一个称为Map.Entry的小嵌套接口,这个Set中的元素的类型。
Collection视图提供了遍历Map的唯一方法。这个例子演示了使用for-each结构遍历Map中键的标准习语:
for (KeyType key : m.keySet())
System.out.println(key);
并使用iterator:
// Filter a map based on some
// property of its keys.
for (Iterator<Type> it = m.keySet().iterator(); it.hasNext(); )
if (it.next().isBogus())
it.remove();
遍历值的习语是类似的。以下是遍历键-值对的习语。
for (Map.Entry<KeyType, ValType> e : m.entrySet())
System.out.println(e.getKey() + ": " + e.getValue());
起初,许多人担心这些习语可能会很慢,因为每次调用 Collection 视图操作时,Map 都必须创建一个新的 Collection 实例。放心:Map 每次被要求返回给定 Collection 视图时都可以返回相同的对象。这正是 java.util 中所有 Map 实现所做的。
对于所有三个 Collection 视图,调用 Iterator 的 remove 操作会从支持的 Map 中删除关联的条目,假设支持的 Map 一开始支持元素的移除。这由前面的过滤习语所说明。
使用 entrySet 视图,还可以在迭代期间通过调用 Map.Entry 的 setValue 方法更改与键关联的值(同样,假设 Map 一开始支持值的修改)。请注意,这些是唯一安全的在迭代期间修改 Map 的方式;如果在迭代进行中以任何其他方式修改底层 Map,行为是未指定的。
Collection 视图支持元素的各种形式的移除 remove、removeAll、retainAll 和 clear 操作,以及 Iterator.remove 操作。(再次强调,这假设支持的 Map 支持元素的移除。)
Collection 视图不支持在任何情况下添加元素。对于 keySet 和 values 视图来说是没有意义的,对于 entrySet 视图来说也是不必要的,因为支持的 Map 的 put 和 putAll 方法提供了相同的功能。
集合视图的花式用法:Map 代数
当应用于 Collection 视图时,批量操作(containsAll、removeAll 和 retainAll)是令人惊讶的强大工具。首先,假设你想知道一个 Map 是否是另一个的子映射 也就是说,第一个 Map 是否包含第二个中的所有键值映射。以下习语可以实现这一点。
if (m1.entrySet().containsAll(m2.entrySet())) {
...
}
类似地,假设你想知道两个 Map 对象是否包含相同键的所有映射。
if (m1.keySet().equals(m2.keySet())) {
...
}
假设你有一个代表属性-值对集合的 Map,以及两个表示必需属性和可允许属性的 Set(可允许属性包括必需属性)。以下代码段确定属性映射是否符合这些约束,并在不符合时打印详细的错误消息。
static <K, V> boolean validate(Map<K, V> attrMap, Set<K> requiredAttrs, Set<K>permittedAttrs) {
boolean valid = true;
Set<K> attrs = attrMap.keySet();
if (! attrs.containsAll(requiredAttrs)) {
Set<K> missing = new HashSet<K>(requiredAttrs);
missing.removeAll(attrs);
System.out.println("Missing attributes: " + missing);
valid = false;
}
if (! permittedAttrs.containsAll(attrs)) {
Set<K> illegal = new HashSet<K>(attrs);
illegal.removeAll(permittedAttrs);
System.out.println("Illegal attributes: " + illegal);
valid = false;
}
return valid;
}
假设你想知道两个 Map 对象共有的所有键。
Set<KeyType>commonKeys = new HashSet<KeyType>(m1.keySet());
commonKeys.retainAll(m2.keySet());
类似的习语可以获得共同的值。
到目前为止,所提出的所有习语都是非破坏性的;也就是说,它们不会修改支持的 Map。以下是一些会修改的。假设你想要删除一个 Map 与另一个 Map 共有的所有键值对。
m1.entrySet().removeAll(m2.entrySet());
假设你想要从一个 Map 中删除所有在另一个中具有映射的键。
m1.keySet().removeAll(m2.keySet());
当你开始在同一批量操作中混合键和值时会发生什么?假设你有一个Map,managers,将公司中的每个员工映射到员工的经理。我们故意对键和值对象的类型保持模糊。只要它们相同就可以。现在假设你想知道所有“个人贡献者”(非经理)是谁。下面的代码片段会告诉你确切想要知道的内容。
Set<Employee> individualContributors = new HashSet<Employee>(managers.keySet());
individualContributors.removeAll(managers.values());
假设你想解雇所有直接向某个经理 Simon 汇报的员工。
Employee simon = ... ;
managers.values().removeAll(Collections.singleton(simon));
请注意,这种习惯用法使用了Collections.singleton,这是一个返回具有单个指定元素的不可变Set的静态工厂方法。
一旦你完成了这个操作,你可能会有一堆员工,他们的经理不再为公司工作(如果 Simon 的直接下属本身是经理的话)。下面的代码将告诉你哪些员工的经理不再为公司工作。
Map<Employee, Employee> m = new HashMap<Employee, Employee>(managers);
m.values().removeAll(managers.keySet());
Set<Employee> slackers = m.keySet();
这个例子有点棘手。首先,它创建了Map的临时副本,并从临时副本中删除所有值为原始Map中键的条目。请记住,原始Map为每个员工都有一个条目。因此,临时Map中剩余的条目包括所有原始Map中值不再是员工的条目。因此,临时副本中的键恰好代表我们正在寻找的员工。
这一节中包含的习语还有很多,但列出它们所有将是不切实际和乏味的。一旦你掌握了它,当你需要时想出正确的习语并不那么困难。
多重映射
Multimap类似于Map,但它可以将每个键映射到多个值。Java 集合框架不包括用于多重映射的接口,因为它们并不常用。使用值为List实例的Map作为多重映射是一件相当简单的事情。下一个代码示例演示了这种技术,它读取一个包含每行一个单词(全部小写)的单词列表,并打印出符合大小标准的所有变位词组。变位词组是一组单词,所有这些单词都包含完全相同的字母,但顺序不同。程序在命令行上接受两个参数:(1)字典文件的名称和(2)要打印出的变位词组的最小大小。不打印包含少于指定最小值的单词的变位词组。
有一个标准的技巧用于找到变位词组:对于字典中的每个单词,将单词中的字母按字母顺序排列(即,重新排列单词的字母以字母顺序)并将条目放入一个多重映射中,将按字母顺序排列的单词映射到原始单词。例如,单词bad会导致将abd映射到bad的条目放入多重映射中。稍加思考就会发现,任何给定键映射到的所有单词形成一个变位词组。遍历多重映射中的键并打印出符合大小约束的每个变位词组是一件简单的事情。
以下程序是这种技术的一个直接实现。
import java.util.*;
import java.io.*;
public class Anagrams {
public static void main(String[] args) {
int minGroupSize = Integer.parseInt(args[1]);
// Read words from file and put into a simulated multimap
Map<String, List<String>> m = new HashMap<String, List<String>>();
try {
Scanner s = new Scanner(new File(args[0]));
while (s.hasNext()) {
String word = s.next();
String alpha = alphabetize(word);
List<String> l = m.get(alpha);
if (l == null)
m.put(alpha, l=new ArrayList<String>());
l.add(word);
}
} catch (IOException e) {
System.err.println(e);
System.exit(1);
}
// Print all permutation groups above size threshold
for (List<String> l : m.values())
if (l.size() >= minGroupSize)
System.out.println(l.size() + ": " + l);
}
private static String alphabetize(String s) {
char[] a = s.toCharArray();
Arrays.sort(a);
return new String(a);
}
}
在一个包含 173,000 个单词的字典文件上运行此程序,最小变位词组大小为八时,会产生以下输出。
9: [estrin, inerts, insert, inters, niters, nitres, sinter,
triens, trines]
8: [lapse, leaps, pales, peals, pleas, salep, sepal, spale]
8: [aspers, parses, passer, prases, repass, spares, sparse,
spears]
10: [least, setal, slate, stale, steal, stela, taels, tales,
teals, tesla]
8: [enters, nester, renest, rentes, resent, tenser, ternes,
treens]
8: [arles, earls, lares, laser, lears, rales, reals, seral]
8: [earings, erasing, gainers, reagins, regains, reginas,
searing, seringa]
8: [peris, piers, pries, prise, ripes, speir, spier, spire]
12: [apers, apres, asper, pares, parse, pears, prase, presa,
rapes, reaps, spare, spear]
11: [alerts, alters, artels, estral, laster, ratels, salter,
slater, staler, stelar, talers]
9: [capers, crapes, escarp, pacers, parsec, recaps, scrape,
secpar, spacer]
9: [palest, palets, pastel, petals, plates, pleats, septal,
staple, tepals]
9: [anestri, antsier, nastier, ratines, retains, retinas,
retsina, stainer, stearin]
8: [ates, east, eats, etas, sate, seat, seta, teas]
8: [carets, cartes, caster, caters, crates, reacts, recast,
traces]
这些单词中有许多看起来有点虚假,但这不是程序的错;它们在字典文件中。这是我们使用的字典文件。它是从公共领域的 ENABLE 基准参考词汇列表派生而来。




浙公网安备 33010602011771号