第十章 图形用户界面程序设计
Java图形用户界面(GUI)程序设计是使用Java编程语言创建交互式应用程序的一种方法。Java提供了许多GUI库,如AWT,Swing和JavaFX,使程序员能够创建复杂的用户界面和应用程序。
以下是编写Java GUI程序的步骤:
- 导入必要的库和包。
- 创建UI组件,如按钮、文本框等。
- 设置UI组件的位置、大小、字体、颜色和其他属性。
- 添加UI组件到GUI容器中,如JFrame或JPanel。
- 创建事件处理程序,如按钮点击事件。
- 注册事件处理程序,使UI组件能够响应事件。
- 在事件处理程序中编写逻辑或业务逻辑代码。
- 启动应用程序,使GUI界面可见。
以下是一个简单的Java GUI程序示例:
import javax.swing.*;
public class MyProgram extends JFrame {
public MyProgram() {
// 设置窗口的标题
setTitle("My Program");
// 创建按钮对象
JButton button = new JButton("Click Me");
// 注册按钮事件处理程序
button.addActionListener(e -> {
// 在按钮被点击时执行的代码
JOptionPane.showMessageDialog(this, "Hello, World!");
});
// 添加按钮到窗口中
add(button);
// 设置窗口大小
setSize(300, 200);
// 窗口居中显示
setLocationRelativeTo(null);
// 设置窗口关闭时退出程序
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 显示窗口
setVisible(true);
}
public static void main(String[] args) {
new MyProgram();
}
}
此程序创建一个窗口,其中包含一个按钮。单击按钮时,将弹出一个消息框,显示“Hello, World!”消息。
10.1 Java用户界面工具包简史
Java用户界面(UI)工具包的发展经历了多个阶段,每个阶段都有其特定的特征和优势,下面是Java用户界面工具包简史。
-
AWT(Abstract Window Toolkit):AWT是Java最早的UI工具包,于1995年首次出现。AWT提供了一组Java类和接口,用于创建和管理应用程序的图形用户界面。AWT是一个跨平台的工具包,能够在各种操作系统和窗口系统上运行。AWT的组件由本地操作系统提供,这使得它们具有良好的外观和性能。但是,AWT缺少一些常见的UI组件,且没有提供任何可定制的外观。
-
Swing:Swing是Java最广泛使用的UI工具包之一,也是AWT的后继者。Swing在1998年推出,是一个基于Java的独立UI工具包,提供了比AWT更多的UI组件,并允许用户自定义UI外观。Swing的组件不是由本地操作系统提供的,而是完全由Java代码实现的。这意味着Swing应用程序的外观和行为在各种平台和操作系统上都是一致的。Swing是跨平台的,可以在各种操作系统和窗口系统上运行。
-
JavaFX:JavaFX是Oracle公司在2007年推出的下一代UI工具包。JavaFX提供了一种图形化的方式来创建丰富的UI应用程序和交互式内容。JavaFX具有可扩展性、高性能和可重用性的特点,它使用Java语言开发,可以与Java平台的其他部分(如Java Runtime环境和Java IDE)无缝集成。JavaFX提供了许多高级UI组件和布局,使得开发丰富的UI应用程序变得更加容易。
虽然Java用户界面工具包已经经历了多个阶段,但它在Java应用程序开发中仍然扮演着重要的角色。无论使用哪个工具包,Java UI开发人员都能够利用Java的跨平台性和可移植性,创建出高质量的用户界面应用程序。
10.2 显示窗体
Java窗体是Java用户界面(UI)中的一个重要部分,用于创建基于图形的应用程序。Java窗体提供了一个可调整大小的容器,用于放置各种UI组件(如按钮、标签、文本框等)。Java窗体提供了很多自定义选项,例如在窗体中添加菜单栏、工具栏、状态栏等。
使用Java窗体可以快速创建基于图形的应用程序,窗体可以作为应用程序的主要容器,提供一个视图来显示应用程序的内容。创建窗体可以通过Java的Swing或JavaFX库中提供的类来实现。
10.2.1 创建窗体
在Java中,创建窗体的一种常见方法是利用Swing框架中的JFrame类。以下是一个简单的示例程序:
import javax.swing.JFrame;
public class MyFrame extends JFrame {
public MyFrame() {
setTitle("My Window"); // 设置窗体标题
setSize(400, 300); // 设置窗体大小
setLocationRelativeTo(null); // 将窗体居中显示
setVisible(true); // 显示窗体
}
public static void main(String[] args) {
MyFrame frame = new MyFrame(); // 创建窗体实例
}
}
在这个示例程序中,我们创建了一个名为MyFrame的类,并继承了JFrame类。在类的构造方法中,我们通过调用一些JFrame类提供的方法来设置窗体的标题、大小、位置和可见性。最后,在main方法中,我们实例化了MyFrame类,从而创建了一个窗体对象。
当我们运行该示例程序时,将会弹出一个名为"My Window"的窗体,大小为400x300,并位于屏幕中央。
10.2.2 窗体属性
属性名 | 描述 |
---|---|
setTitle() | 设置窗体标题 |
setBounds() | 设置窗体在屏幕上的位置和大小 |
setResizable() | 设置窗体是否可改变大小 |
setLayout() | 设置窗体的布局管理器 |
getContentPane() | 返回窗体的主体部分 |
setContentPane() | 设置窗体的主体部分 |
setBackground() | 设置窗体的背景颜色 |
setForeground() | 设置窗体的前景颜色 |
setVisible() | 设置窗体是否可见 |
setDefaultCloseOperation() | 设置窗体默认关闭行为 |
setIconImage() | 设置窗体的图标 |
setAlwaysOnTop() | 设置窗体是否始终处于顶层 |
setModal() | 设置窗体是否为模态窗口 |
setOpacity() | 设置窗体的透明度 |
setShape() | 设置窗体的形状 |
setUndecorated() | 设置窗体是否显示边框和标题栏 |
下面是一个简单的示例代码,包含了一些常见的控制元素和属性。
import javax.swing.*;
import java.awt.*;
public class MyFrame extends JFrame {
public MyFrame() {
// 设置窗口标题
setTitle("My Frame");
// 设置窗口大小
setSize(400, 300);
// 设置窗口位置
setLocation(100, 100);
// 设置窗口布局
setLayout(new BorderLayout());
// 添加菜单栏
JMenuBar menuBar = new JMenuBar();
JMenu fileMenu = new JMenu("File");
JMenuItem openItem = new JMenuItem("Open");
JMenuItem saveItem = new JMenuItem("Save");
fileMenu.add(openItem);
fileMenu.add(saveItem);
menuBar.add(fileMenu);
setJMenuBar(menuBar);
// 添加按钮
JButton btn = new JButton("Click me");
add(btn, BorderLayout.CENTER);
// 添加标签
JLabel label = new JLabel("This is a label");
add(label, BorderLayout.NORTH);
// 添加复选框和单选框
JCheckBox checkBox = new JCheckBox("Checkbox");
add(checkBox, BorderLayout.WEST);
JRadioButton radioBtn1 = new JRadioButton("Radio button 1");
JRadioButton radioBtn2 = new JRadioButton("Radio button 2");
ButtonGroup radioGroup = new ButtonGroup();
radioGroup.add(radioBtn1);
radioGroup.add(radioBtn2);
add(radioBtn1, BorderLayout.EAST);
add(radioBtn2, BorderLayout.EAST);
// 添加文本框和密码框
JTextField textField = new JTextField("Text field");
add(textField, BorderLayout.SOUTH);
JPasswordField passwordField = new JPasswordField("Password field");
add(passwordField, BorderLayout.SOUTH);
// 设置可见性
setVisible(true);
}
public static void main(String[] args) {
new MyFrame();
}
}
10.3 在组件中显示信息
要在Java组件中显示信息,您需要使用一个标签,如JLabel或JTextPane,并使用setText()方法设置要显示的文本。然后将此标签添加到您要显示的组件中。
例如,在以下代码中,我们使用JLabel设置文本“Hello World!”并将其添加到JFrame中的面板上:
import javax.swing.*;
public class MyFrame extends JFrame {
private JLabel label;
public MyFrame() {
label = new JLabel();
label.setText("Hello World!");
JPanel panel = new JPanel();
panel.add(label);
add(panel);
pack();
setVisible(true);
}
public static void main(String[] args) {
new MyFrame();
}
}
10.3.1 处理2D图形
一、Graphics类
Graphics类是Java AWT(抽象窗口工具集)包中的一个类,它提供了一些方法来绘制图形、文本和图像。通过使用Graphics类,可以创建各种各样的用户界面和其他可视化组件,例如:按钮、文本框、标签等。
Graphics类包含了许多绘图方法,例如:drawLine(画线)、drawRect(画矩形)、drawOval(画椭圆)等等。可以使用这些方法来创建自定义的组件,或直接在画布上绘制矢量图形。
除了绘图方法,Graphics类还提供了一些方法来设置颜色、字体、显示图像等等。使用这些方法,可以轻松地实现各种不同的视觉效果,使应用程序更加丰富多彩。
总之,Graphics类是Java中重要的绘图工具之一,可以帮助开发者轻松地创建各种视觉元素和自定义组件,并为应用程序提供各种视觉效果。
下面是使用Java的Graphics类绘制直线、圆形、椭圆、五角星和六边形的代码:
import java.awt.Graphics;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class DrawingShapes extends JPanel {
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
// 画直线
g.drawLine(20, 20, 100, 20);
// 画圆形
g.drawOval(150, 20, 80, 80);
// 画椭圆
g.drawOval(300, 20, 80, 40);
// 画五角星
int[] xPoints = { 150, 170, 220, 180, 190 };
int[] yPoints = { 150, 200, 200, 170, 120 };
g.drawPolygon(xPoints, yPoints, 5);
// 画六边形
int[] xPoints2 = { 300, 350, 400, 400, 350, 300 };
int[] yPoints2 = { 150, 170, 150, 120, 100, 120 };
g.drawPolygon(xPoints2, yPoints2, 6);
}
public static void main(String[] args) {
JFrame frame = new JFrame("Drawing Shapes");
frame.add(new DrawingShapes());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(500, 300);
frame.setVisible(true);
}
}
这个代码将创建一个名为“Drawing Shapes”的窗口,并在窗口中绘制直线、圆形、椭圆、五角星和六边形。
二、Java 2D库
Java 2D库是Java中用于创建图形和图像的强大库。它提供了一组API,可用于将几何形状、文本、图像和颜色合并,以创建复杂的图形和图像。Java 2D库包括以下功能:
-
绘制基本图形,如直线、矩形、椭圆和多边形
-
绘制图像和文本
-
进行透明度处理
-
提供多种渲染器,如抗锯齿和图片缩放
-
支持复合形状和转换
-
以坐标系为基础进行绘制
Java 2D库的编程模型与Java AWT非常类似。它主要依赖于图形、形状和Paint对象。要使用Java 2D库,需要创建一个Graphics2D对象,这是一个Graphics对象的子类。接下来,您可以使用该对象调用各种绘图方法。
以下是一个绘制简单矩形和椭圆的示例程序:
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Java2DExample extends JPanel {
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
Rectangle2D rect = new Rectangle2D.Double(50, 50, 200, 150);
g2d.setPaint(Color.BLUE);
g2d.fill(rect);
Ellipse2D ellipse = new Ellipse2D.Double(100, 100, 100, 100);
g2d.setPaint(Color.YELLOW);
g2d.fill(ellipse);
}
public static void main(String[] args) {
JFrame frame = new JFrame("Java 2D Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new Java2DExample());
frame.setSize(300, 250);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
这个示例程序将创建一个名为“Java 2D Example”的窗口,并在窗口中绘制一个矩形和一个椭圆。
三、Rectangle2D类
Rectangle2D类是Java中的一个基本图形类,它代表二维矩形。它是一个抽象类,有两个子类:Rectangle2D.Float和Rectangle2D.Double,分别代表为浮点数和双精度浮点数类型的矩形。Rectangle2D类提供了一些方法,可以用于计算和操作矩形的大小和位置。下面是一些Rectangle2D类的常用方法:
-
double getX()
:返回矩形左上角的x坐标。 -
double getY()
:返回矩形左上角的y坐标。 -
double getWidth()
:返回矩形的宽度。 -
double getHeight()
:返回矩形的高度。 -
void setFrame(double x, double y, double w, double h)
:设置矩形的位置和大小。 -
void setRect(double x, double y, double w, double h)
:设置矩形的位置和大小。 -
boolean contains(double x, double y)
:判断点(x, y)是否在矩形内。 -
boolean contains(Rectangle2D r)
:判断矩形r是否完全在当前矩形内。 -
boolean intersects(Rectangle2D r)
:判断当前矩形是否与矩形r相交。
下面是一个简单的示例程序,演示了如何使用Rectangle2D类:
import java.awt.geom.Rectangle2D;
public class Rectangle2DExample {
public static void main(String[] args) {
Rectangle2D rect = new Rectangle2D.Double(50, 50, 100, 80);
System.out.println("x: " + rect.getX());
System.out.println("y: " + rect.getY());
System.out.println("width: " + rect.getWidth());
System.out.println("height: " + rect.getHeight());
rect.setFrame(70, 70, 120, 100);
System.out.println("x: " + rect.getX());
System.out.println("y: " + rect.getY());
System.out.println("width: " + rect.getWidth());
System.out.println("height: " + rect.getHeight());
}
}
这个示例程序创建了一个Rectangle2D对象,设置了它的位置和大小,并使用getX()、getY()、getWidth()和getHeight()打印它的属性。然后,使用setFrame()方法更改了矩形的位置和大小,并再次打印其属性。运行程序,将看到以下输出:
x: 50.0
y: 50.0
width: 100.0
height: 80.0
x: 70.0
y: 70.0
width: 120.0
height: 100.0
在本示例中,我们创建了一个名为rect
的矩形对象,它的左上角坐标为(50, 50),宽度为100,高度为80。然后我们使用setFrame()方法更改了矩形的位置和大小,将其移动到(70, 70),并将宽度和高度增加到120和100。最后,我们打印了矩形的属性,确认更改已生效。
四、Ellipse2D类
Ellipse2D类是Java中的一个图形类,表示一个椭圆形状。它是Shape接口的实现类,提供了一系列操作椭圆形状的方法。
Ellipse2D类有两个子类:Ellipse2D.Double和Ellipse2D.Float。Double和Float分别表示椭圆的坐标和尺寸是double类型和float类型。
常用的方法:
-
getCenterX() 和 getCenterY():获取椭圆的中心点坐标。
-
getWidth() 和 getHeight():获取椭圆的宽度和高度。
-
setFrame(double x, double y, double w, double h):设置椭圆的坐标和尺寸。
-
contains(double x, double y):判断点(x,y)是否在椭圆内。
-
intersects(Rectangle2D r):判断矩形r是否与椭圆相交。
-
createIntersection(Rectangle2D r)和createUnion(Rectangle2D r):创建与矩形r相交或相并的椭圆形状。
-
fill(Graphics2D g)和draw(Graphics2D g):用Graphics2D对象画出填充或轮廓的椭圆形状。
使用示例:
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class MyPanel extends JPanel {
public void paint(Graphics g) {
super.paint(g);
Graphics2D g2d = (Graphics2D) g;
Ellipse2D ellipse = new Ellipse2D.Double(50, 50, 100, 200);
g2d.fill(ellipse);
}
public static void main(String[] args) {
JFrame frame = new JFrame("Ellipse Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new MyPanel());
frame.setSize(300, 300);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
这个示例创建了一个椭圆形状,并用Graphics2D对象画出了填充的椭圆。Ellipse2D.Double的四个参数分别表示椭圆左上角的坐标、椭圆的宽度和高度。
10.3.2 使用颜色
在Java GUI中,可以使用java.awt.Color类来设置颜色。Color类提供了一些常用的颜色常量,如RED、GREEN、BLUE、YELLOW等等,也可以通过RGB值来自定义颜色。
示例代码:
import java.awt.Color;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class MyPanel extends JPanel {
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.RED); // 设置颜色为红色
g.fillRect(50, 50, 100, 100); // 填充一个矩形
}
public static void main(String[] args) {
JFrame frame = new JFrame("Color Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new MyPanel());
frame.setSize(300, 300);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
在这个例子中,我们使用g.setColor()
来设置颜色为红色,然后使用g.fillRect()
填充一个矩形。可以根据需要在paintComponent()方法中设置不同的颜色和绘制不同的图形。
以下是Graphics2D类中使用颜色的方法示例
- 设置画笔颜色:
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(Color.RED);
- 绘制填充矩形:
g2d.setColor(Color.GREEN);
g2d.fillRect(10, 10, 200, 100);
- 绘制空心矩形:
g2d.setColor(Color.BLUE);
g2d.drawRect(10, 10, 200, 100);
- 绘制填充椭圆:
g2d.setColor(Color.ORANGE);
g2d.fillOval(10, 10, 200, 100);
- 绘制空心椭圆:
g2d.setColor(Color.PINK);
g2d.drawOval(10, 10, 200, 100);
- 绘制填充圆弧:
g2d.setColor(Color.YELLOW);
g2d.fillArc(10, 10, 200, 100, 0, 90);
- 绘制空心圆弧:
g2d.setColor(Color.MAGENTA);
g2d.drawArc(10, 10, 200, 100, 0, 90);
- 绘制多边形:
g2d.setColor(Color.CYAN);
int[] xPoints = {50, 100, 150};
int[] yPoints = {100, 50, 100};
g2d.fillPolygon(xPoints, yPoints, 3);
- 绘制文本:
g2d.setColor(Color.BLACK);
g2d.drawString("Hello, World!", 10, 10);
10.3.3 使用字体
在Java窗体中使用字体涉及以下类:
-
java.awt.Font:表示在绘制文本时使用的字体,可以设置字体名称、样式和大小等属性。
-
javax.swing.JLabel:用于在GUI中显示文本或图像,可以通过设置字体属性来改变显示文本的字体。
-
javax.swing.JTextArea:用于在GUI中显示多行文本,可以通过设置字体属性来改变文本的字体。
-
javax.swing.JTextField:用于在GUI中显示单行文本,可以通过设置字体属性来改变文本的字体。
-
javax.swing.JButton:用于在GUI中显示按钮,可以通过设置字体属性来改变按钮上的文本的字体。
-
javax.swing.JComboBox:用于在GUI中显示下拉列表,可以通过设置字体属性来改变列表项的字体。
-
javax.swing.JList:用于在GUI中显示列表,可以通过设置字体属性来改变列表项的字体。
-
javax.swing.JTable:用于在GUI中显示表格,可以通过设置字体属性来改变表格中文本的字体。
以下是一个简单的Java窗体程序,使用了字体相关的类和方法,包括:JFrame、JLabel、Font、Dimension等。
import javax.swing.*;
import java.awt.*;
public class FontExample extends JFrame {
public FontExample() {
// 设置窗体大小和标题
this.setSize(new Dimension(400, 300));
this.setTitle("Font Example");
// 创建一个标签,设置字体和文本
JLabel label = new JLabel("使用了自定义字体的标签");
Font customFont = new Font("微软雅黑", Font.BOLD, 24);
label.setFont(customFont);
// 将标签添加到窗体中央
this.add(label, BorderLayout.CENTER);
// 显示窗体
this.setVisible(true);
}
public static void main(String[] args) {
new FontExample();
}
}
10.4 事件处理
在Java中,事件处理是指在应用程序中发生事件时,程序能够捕获这些事件并响应它们。Java事件处理机制基于观察者模式,即当前存在一个事件源(如一个按钮或文本框),并且程序可以注册监听器来监听该事件源上发生的任何事件。
10.4.1 基本事件处理的概念
Java基本事件处理是指在Java程序中,对各种事件进行处理的机制。事件是指由外部环境或用户发出的信号,例如鼠标点击、键盘输入、窗口关闭等。Java提供了一套事件处理机制,程序员可以通过定义事件监听器,注册监听器来处理各种事件。
Java事件处理机制主要由以下几个部分构成:
-
事件源:即产生事件的对象,例如按钮、文本框等。
-
事件:即发生的事件,例如鼠标点击事件、键盘事件等。
-
事件监听器:即事件发生后要执行的方法,例如鼠标点击后要执行的方法。
-
事件分发器:即将事件分发给对应的监听器。
Java中的事件处理分为两种方式:基于接口的事件处理机制和基于注解的事件处理机制。
基于接口的事件处理机制是通过实现事件监听器接口,实现接口中的方法来处理事件。例如,实现ActionListener接口中的actionPerformed方法来处理按钮点击事件。
基于注解的事件处理机制是通过在事件处理方法上添加注解,指定事件类型和事件源来处理事件。例如,@ActionListenerFor注解可以指定按钮点击事件和按钮对象来处理按钮点击事件。
无论是基于接口还是基于注解的事件处理方式,都是为了让程序员能够方便地处理各种事件,为用户提供更好的交互体验。
以下是一个基本的Java事件处理程序的示例:
import java.awt.*;
import java.awt.event.*;
public class ButtonExample extends Frame implements ActionListener {
Button btn;
public ButtonExample() {
btn = new Button("Click Me!");
add(btn);
btn.addActionListener(this); // 注册监听器对象
setSize(300, 300);
setVisible(true);
}
public void actionPerformed(ActionEvent e) {
if (e.getSource() == btn) { // 判断事件源是否为btn
System.out.println("Button Clicked!");
}
}
public static void main(String[] args) {
new ButtonExample();
}
}
在上面的示例中,我们创建了一个名为ButtonExample的窗体,并将一个按钮添加到窗体中。我们还实现了ActionListener接口,并将ButtonExample类本身作为监听器对象。我们将监听器对象注册到按钮对象中,并实现了actionPerformed()方法来处理按钮单击事件。在该方法中,我们使用getSource()方法来确定事件源,如果源是按钮,则打印“Button Clicked!”的消息。最后,我们在main()方法中创建ButtonExample对象,并显示窗体。
10.4.2 实例:处理按钮点击事件
下面是一个Java处理按钮点击事件的示例代码:
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class ButtonClickEventExample implements ActionListener {
private JButton button;
public ButtonClickEventExample() {
// 创建一个按钮对象
button = new JButton("点击我");
// 给按钮添加点击事件监听器
button.addActionListener(this);
}
public void actionPerformed(ActionEvent e) {
// 处理按钮点击事件
JOptionPane.showMessageDialog(null, "你点击了按钮");
}
public static void main(String[] args) {
// 创建一个窗口
JFrame frame = new JFrame("按钮点击事件示例");
// 创建一个按钮对象
ButtonClickEventExample example = new ButtonClickEventExample();
JButton button = example.button;
// 添加按钮到窗口
frame.add(button);
// 设置窗口大小和位置
frame.setSize(400, 300);
frame.setLocationRelativeTo(null);
// 显示窗口
frame.setVisible(true);
}
}
这个示例程序创建了一个按钮对象,并给按钮添加了一个点击事件监听器。当按钮被点击时,程序会弹出一个消息框提示用户已点击了按钮。
10.4.3 简洁的指定监视器
在 Java 基本事件处理中,可以使用 lambda 表达式指定监听器来实现简洁的代码。例如,在设置 ActionListener 监听器时,可以使用以下代码:
button.addActionListener(event -> {
// 处理按钮被点击后的事件
});
这里,我们使用 lambda 表达式来创建一个 ActionListener 监听器,并在其中定义按钮被点击后的事件处理代码。这样,我们就可以非常简洁地指定监听器,而无需使用传统的匿名内部类语法。类似地,也可以在其他事件处理中使用 lambda 表达式指定监听器。
以下是一个简单的代码示例,演示了这两种方法之间的区别:
// 使用lambda表达式
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.forEach(n -> System.out.println(n));
// 不使用lambda表达式
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
for (int n : numbers) {
System.out.println(n);
}
上面的代码展示了如何使用lambda表达式和不使用它来迭代一个整数列表。使用lambda表达式时,我们可以将迭代逻辑作为参数传递给forEach方法。这使得代码更加简洁和易于阅读。
不使用lambda表达式时,我们需要使用for循环来遍历整数列表,并在循环体中打印每个整数。这种方法需要更多的代码,并且可能会导致代码中出现更多的错误。
10.4.4 适配器类
Java窗体适配器类是一个抽象类,主要用于创建窗体事件处理器。它允许开发人员只实现他们需要的事件处理方法,而无需全部实现所有事件处理方法。这大大简化了事件处理器的开发过程。
Java窗体适配器类的常见事件处理方法包括:
- windowOpened():当窗口打开时调用。
- windowClosing():当用户试图关闭窗口时调用。
- windowClosed():当窗口已经关闭时调用。
- windowActivated():当窗口被激活时调用。
- windowDeactivated():当窗口被取消激活时调用。
- windowIconified():当窗口被最小化时调用。
- windowDeiconified():当窗口从最小化还原时调用。
开发人员只需要继承窗体适配器类,并重写需要处理事件的方法即可。例如,如果我们只需要处理窗口关闭事件,我们可以创建一个类继承窗体适配器类,然后重写windowClosing()方法。
以下是一个简单的Java窗体事件处理器示例:
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class MyWindowAdapter extends WindowAdapter {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
}
在上面的代码中,我们创建了一个名为MyWindowAdapter的类,它继承了WindowAdapter类,并重写了windowClosing()方法。在这个方法中,我们只是简单地退出程序。这样,当用户试图关闭窗口时,窗口事件处理器将自动调用windowClosing()方法,程序将退出。
以下是Java窗体适配器类的常见事件处理方法代码:
- 窗口关闭事件处理
window.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
// 当窗口关闭时执行的代码
}
});
- 窗口最小化事件处理
frame.addWindowStateListener(new WindowAdapter() {
public void windowStateChanged(WindowEvent e) {
if ((e.getNewState() & Frame.ICONIFIED) == Frame.ICONIFIED) {
// 当窗口最小化时执行的代码
}
}
});
- 窗口打开事件处理
frame.addWindowListener(new WindowAdapter() {
public void windowOpened(WindowEvent e) {
// 当窗口打开时执行的代码
}
});
- 窗口激活事件处理
frame.addWindowFocusListener(new WindowAdapter() {
public void windowGainedFocus(WindowEvent e) {
// 当窗口获得焦点时执行的代码
}
});
- 滚轮事件处理
scrollPane.addMouseWheelListener(new MouseAdapter() {
public void mouseWheelMoved(MouseWheelEvent e) {
int notches = e.getWheelRotation();
// 当滚轮滚动时执行的代码
}
});
10.4.5 动作
Java 事件处理的动作包括以下几步:
-
定义事件监听器接口:该接口负责监听某个事件并做出相应的处理。
-
创建事件源对象:该对象负责产生事件,可以是按钮、文本框、菜单等 GUI 组件。
-
注册事件监听器:将事件监听器注册到事件源对象上,以便监听器可以接收到事件并作出相应的处理。
-
实现事件监听器接口:编写具体的事件处理方法,如按钮点击时执行的代码。
-
激发事件:指定何时激活事件,如当用户点击按钮时激发事件。
-
处理事件:事件监听器接收到事件后,执行相关代码逻辑来处理事件。
-
可选的事件反馈:如果需要,可以给用户提供反馈,如弹出对话框、显示提示信息等。
以下是一个简单的 Java 事件处理的动作代码实例:
- 定义事件监听器接口:
import java.awt.event.ActionListener;
public interface MyButtonListener extends ActionListener {
void onClick(); // 自定义的事件处理方法
}
- 创建事件源对象:
import javax.swing.*;
public class MyButton extends JButton {
private MyButtonListener listener;
public MyButton(String label) {
super(label);
}
public void setListener(MyButtonListener listener) {
this.listener = listener;
}
public void click() {
if (listener != null) {
listener.onClick(); // 触发自定义的事件处理方法
}
}
}
- 注册事件监听器:
MyButton button = new MyButton("Click Me");
button.setListener(new MyButtonListener() {
@Override
public void actionPerformed(ActionEvent e) {
button.click(); // 触发自定义事件
}
@Override
public void onClick() {
System.out.println("Button clicked!"); // 自定义事件处理方法,输出信息
}
});
- 实现事件监听器接口:
@Override
public void onClick() {
System.out.println("Button clicked!"); // 实现自定义事件处理方法,输出信息
}
- 激发事件:
button.click(); // 手动调用自定义事件
- 处理事件:
// 自定义事件处理方法,输出信息
@Override
public void onClick() {
System.out.println("Button clicked!");
}
- 可选的事件反馈:
// 自定义事件处理方法,弹出对话框反馈
@Override
public void onClick() {
JOptionPane.showMessageDialog(null, "Button clicked!");
}
Swing包中的动作方法包括以下几个:
-
addActionListener(ActionListener listener):为该组件添加动作监听器。
-
removeActionListener(ActionListener listener):从该组件中移除指定的动作监听器。
-
setEnabled(boolean b):启用或禁用该组件的动作事件。
-
actionPerformed(ActionEvent e):处理动作事件的方法,由实现了ActionListener接口的类来实现。
-
getActionCommand():获取动作命令名称。
-
setActionCommand(String command):设置动作命令名称。
-
setAccelerator(KeyStroke keyStroke):设置加速键。
-
setMnemonic(int mnemonic):设置助记符。
-
setEnabled(boolean enabled):启用或禁用该动作。
-
putValue(String key, Object value):设置给定键的值。
这些动作方法可以在Swing组件中使用,例如按钮、菜单项、文本框等,以添加、移除、处理动作事件等操作。
下面是一个简单的Swing应用程序,它演示了如何使用动作方法:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class SwingActionExample {
private JFrame frame;
private JButton button1;
private JButton button2;
public SwingActionExample() {
frame = new JFrame("Swing Action Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 创建一个Action对象
Action action = new CustomAction("Click me!");
// 创建按钮并将Action对象关联到它
button1 = new JButton(action);
// 创建另一个按钮并手动添加动作监听器
button2 = new JButton("Click me too!");
button2.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(frame, "You clicked me too!");
}
});
// 将按钮添加到框架中
Container contentPane = frame.getContentPane();
contentPane.add(button1, BorderLayout.NORTH);
contentPane.add(button2, BorderLayout.SOUTH);
frame.pack();
frame.setVisible(true);
}
/**
* 自定义Action类。
*/
class CustomAction extends AbstractAction {
public CustomAction(String text) {
super(text);
}
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(frame, "You clicked me!");
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
new SwingActionExample();
}
});
}
}
在这个例子中,我们创建了两个按钮。其中一个使用 AbstractAction
的子类 CustomAction
来定义按钮的动作,另一个使用手动添加监听器的方式来处理按钮的动作。
CustomAction
类覆盖了 actionPerformed
方法,该方法在按钮被点击时被调用。在这个例子中,我们展示了一个简单的弹出消息对话框来告诉用户按钮已经被点击。
当用户点击第一个按钮时,它将触发 CustomAction
中的 actionPerformed
方法,而当用户点击第二个按钮时,它将触发我们手动添加的监听器中的 actionPerformed
方法。
请注意,我们在 CustomAction
构造函数中传递的字符串参数是按钮的文本。这个文本将用于显示在按钮上。如果您希望使用图标或其他自定义组件来代替文本,您可以在 CustomAction
类中添加相应的代码来实现。
10.4.6 鼠标事件
Java事件窗口的鼠标事件包括以下常用的5种事件:
-
mouseClicked(MouseEvent e)
:当鼠标按钮在组件上按下并释放时调用此方法。public void mouseClicked(MouseEvent e) { // 处理鼠标单击事件的代码 }
-
mousePressed(MouseEvent e)
:当鼠标按钮在组件上按下时调用此方法。public void mousePressed(MouseEvent e) { // 处理鼠标按下事件的代码 }
-
mouseReleased(MouseEvent e)
:当鼠标按钮在组件上释放时调用此方法。public void mouseReleased(MouseEvent e) { // 处理鼠标释放事件的代码 }
-
mouseEntered(MouseEvent e)
:当鼠标进入组件时调用此方法。public void mouseEntered(MouseEvent e) { // 处理鼠标进入事件的代码 }
-
mouseExited(MouseEvent e)
:当鼠标离开组件时调用此方法。public void mouseExited(MouseEvent e) { // 处理鼠标离开事件的代码 }
下面是一个用于处理鼠标事件的示例代码:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class MouseEventExample extends JFrame implements MouseListener {
private JLabel label;
public MouseEventExample() {
super("Mouse Event Example");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
label = new JLabel("Click me!");
label.addMouseListener(this);
Container contentPane = getContentPane();
contentPane.add(label, BorderLayout.CENTER);
pack();
setVisible(true);
}
public void mouseClicked(MouseEvent e) {
label.setText("You clicked me!");
}
public void mousePressed(MouseEvent e) {
// 按下鼠标按钮时的处理代码
}
public void mouseReleased(MouseEvent e) {
// 释放鼠标按钮时的处理代码
}
public void mouseEntered(MouseEvent e) {
// 鼠标进入组件时的处理代码
}
public void mouseExited(MouseEvent e) {
// 鼠标离开组件时的处理代码
}
public static void main(String[] args) {
new MouseEventExample();
}
}
在这个例子中,我们创建了一个 JLabel
组件,并将它添加到了应用程序的内容面板中。我们还将 MouseEventExample
类实现了 MouseListener
接口,并覆盖了接口中的所有方法以处理鼠标事件。在这里,我们只关心鼠标单击事件,因此我们在 mouseClicked
方法中将标签的文本设置为“你点击了我!”。
当用户单击标签时,将触发 mouseClicked
方法的调用,并更新标签的文本。如果您想要处理其他鼠标事件,您可以改为在相应的方法中添加您的代码。
Java中的光标样式常量位于java.awt包中,常用常量如下:
光标样式常量 | 描述 |
---|---|
DEFAULT_CURSOR | 默认光标,通常为箭头。 |
CROSSHAIR_CURSOR | 十字形光标。 |
TEXT_CURSOR | I形光标,表示可以在文本中输入。 |
WAIT_CURSOR | 等待光标,通常为一个表盘或沙漏形状。 |
SW_RESIZE_CURSOR | 西南角调整大小光标,表示可以通过点击该边框拉伸窗口的大小。 |
SE_RESIZE_CURSOR | 东南角调整大小光标,表示可以通过点击该边框拉伸窗口的大小。 |
NW_RESIZE_CURSOR | 西北角调整大小光标,表示可以通过点击该边框拉伸窗口的大小。 |
NE_RESIZE_CURSOR | 东北角调整大小光标,表示可以通过点击该边框拉伸窗口的大小。 |
N_RESIZE_CURSOR | 北边调整大小光标,表示可以通过点击该边框拉伸窗口的大小。 |
S_RESIZE_CURSOR | 南边边调整大小光标,表示可以通过点击该边框拉伸窗口的大小。 |
W_RESIZE_CURSOR | 西边调整大小光标,表示可以通过点击该边框拉伸窗口的大小。 |
E_RESIZE_CURSOR | 东边调整大小光标,表示可以通过点击该边框拉伸窗口的大小。 |
HAND_CURSOR | 手形光标,表示悬停或点击操作会执行某些操作或打开某些链接。 |
MOVE_CURSOR | 移动光标,表示鼠标拖动一个对象时的光标。 |
CUSTOM_CURSOR | 自定义光标,可以使用图像或其他自定义形状创建一个光标。 |
可以使用以下代码设置光标:
// 设置光标为手形
Component.setCursor(new Cursor(Cursor.HAND_CURSOR));
10.4.7 AWT事件的继承层次
AWT事件的继承层次如下所示:
java.util.EventObject
└── java.awt.AWTEvent
├── java.awt.event.ActionEvent
│ ├── java.awt.event.TextEvent(TextEvent已不建议使用)
│ ├── java.awt.event.ItemEvent
│ ├── java.awt.event.KeyEvent
│ ├── java.awt.event.MouseEvent
│ ├── java.awt.event.FocusEvent
│ └── java.awt.event.WindowEvent
└── java.awt.event.AdjustmentEvent
├── java.awt.event.ContainerEvent
│ ├── java.awt.event.ComponentEvent
│ │ ├── java.awt.event.InputMethodEvent
│ │ ├── java.awt.event.HierarchyEvent
│ │ └── java.awt.event.InvocationEvent
│ └── java.awt.event.FocusEvent
└── java.awt.event.PaintEvent
其中,java.util.EventObject是所有AWT事件的基类,java.awt.AWTEvent则是所有AWT事件类的基类。AWT事件根据事件源和事件类型的不同,分为多种不同的子类,例如ActionEvent、KeyEvent、MouseEvent等等。每个子类都可以包含与其对应的事件源和事件类型相关的信息。
Java.wat.event包中的语义事件类包括:
-
SemanticAnnotationEvent:表示语义注释事件,用于在代码中添加注释以描述代码中的语义信息。
-
SemanticErrorEvent:表示语义错误事件,用于在编译时检测到代码中的语义错误。
-
SemanticModelEvent:表示语义模型事件,用于表示代码的语义模型,并可用于代码分析和重构等操作。
-
SemanticValidationEvent:表示语义验证事件,用于在代码编译或运行时对代码进行语义验证。
这些事件类可用于开发代码分析、语义理解、智能化开发工具等应用。
注:Java事件处理的主要内容如下:
类型 | 描述 |
---|---|
事件 | 操作或情况的发生,可以是用户操作、系统崩溃或其他情况 |
事件源 | 产生事件的对象,例如按钮、文本框等 |
事件监听器 | 监听事件并执行相应操作的对象 |
事件处理器 | 负责处理事件并执行相应操作的代码 |
事件模型 | 事件源产生事件、事件监听器监听事件、事件处理器处理事件的整体架构 |
Java事件处理的基本流程如下:
- 创建事件源对象。
- 创建事件监听器对象,并将其注册到事件源中。
- 通过事件模型实现事件源对象的事件监听。
- 当事件源对象产生相关事件时,事件监听器将收到通知并执行相应操作。
Java中常用的事件包括:
事件 | 描述 |
---|---|
ActionEvent | 按钮、菜单等组件上发生的动作事件 |
KeyEvent | 键盘事件,如按键、释放键、键击等 |
MouseEvent | 鼠标事件,如单击、双击、拖拽等 |
WindowEvent | 窗口事件,如打开、关闭、最小化等 |
ComponentEvent | 组件事件,如添加、移除、显示、隐藏等 |
9.5 首选项API
Java 首选项API是Java平台中的标准API之一,它提供了一种机制来存储和访问应用程序的配置信息。使用首选项API,应用程序可以将用户的首选项、设置和其他配置信息保存在应用程序的上下文中。首选项API提供了一个抽象层,使得应用程序可以在不同的操作系统上使用相同的代码访问配置信息。
Java首选项API包括两个核心类:Preferences和PreferencesFactory。Preferences类表示应用程序的一组配置信息,例如键值对。PreferencesFactory类是一个抽象工厂类,用于创建Preferences实例。Java平台提供了两个实现:FileSystemPreferences和RegistryPreferences。FileSystemPreferences实现使用文件系统来存储配置信息,而RegistryPreferences实现使用Windows注册表来存储配置信息。
使用Java首选项API,应用程序可以:
- 保存和读取用户的配置信息
- 存储和读取应用程序的默认配置信息
- 在不同的平台上使用相同的代码来存储和读取配置信息
- 管理多个配置文件或配置信息集合
Java首选项API常用于桌面应用程序和Java Web应用程序。它可以帮助应用程序更好地适应不同的用户和环境需求,提高应用程序的可配置性和可重用性。
Java首选项API | 描述 |
---|---|
Preferences | 表示Java首选项节点的根对象 |
Preferences.userRoot() | 返回用户首选项根节点 |
Preferences.systemRoot() | 返回系统首选项根节点 |
Preferences.node(String path) | 返回具有给定路径的节点。如果该路径不存在,则会创建节点 |
Preferences.nodeExists(String path) | 如果具有给定路径的节点存在,则返回true |
Preferences.get(String key, String defaultValue) | 返回具有给定键的首选项的值 |
Preferences.put(String key, String value) | 设置具有给定键的首选项的值 |
Preferences.remove(String key) | 删除具有给定键的首选项 |
Preferences.clear() | 删除首选项树中的所有首选项 |
Preferences.childrenNames() | 返回当前节点的所有子节点的名称 |
Preferences.parent() | 返回该节点的父节点 |
Preferences.absolutePath() | 返回此节点的绝对路径名称 |
Preferences.exportNode(OutputStream outputStream) | 将该节点及其子树以XML格式导出到给定的输出流中 |
Preferences.importNode(InputStream inputStream) | 将XML格式的首选项节点导入到此节点下的子树中。输入流必须是由导出节点方法生成的。 |
Preferences.addPreferenceChangeListener(PreferenceChangeListener pcl) | 注册首选项更改侦听器,该侦听器将在此节点或其任何子节点中的首选项发生更改时被调用 |
Preferences.removePreferenceChangeListener(PreferenceChangeListener pcl) | 删除首选项更改侦听器 |
第十一章 Swing 用户界面组件
Swing 用户界面组件是一组 Java 类,用于创建漂亮、可定制和交互式的 GUI 应用程序。以下是一些常用的 Swing 组件:
-
JButton:创建一个带有文本或图标的按钮。
-
JLabel:显示文本或图像。
-
JTextField:单行文本输入框。
-
JTextArea:多行文本输入框。
-
JCheckBox:复选框。
-
JRadioButton:单选按钮。
-
JList:显示一个列表。
-
JComboBox:下拉选择框。
-
JTable:表格。
-
JToolBar:工具栏。
-
JMenu:菜单。
-
JMenuBar:菜单栏。
-
JPopupMenu:弹出菜单。
-
JTree:树形结构。
这些组件可以通过适当的布局管理器放置在容器中,以形成一个完整的用户界面。Swing 还提供了许多其他的组件和特性,如对话框、滚动条、拖放等。它还允许您自定义组件的外观和行为,以满足特定需求。
11.1 Swing和模型-视图-控制器设计模式
Java Swing是Java编程语言中的一套GUI(图形用户界面)工具包。它提供了许多组件,例如按钮、标签、文本框、列表、表格等等。Swing还提供了布局管理器来帮助开发人员将组件放置在合适的位置。Swing是基于模型-视图-控制器(MVC)设计模式构建的。
模型-视图-控制器(MVC)是一种常用的软件设计模式,它将应用程序分解成三个部分:
-
模型:模型是应用程序中的核心组件,它通常表示应用程序中的数据。模型负责存储、检索和更新数据。
-
视图:视图是应用程序中的用户界面,它通常包含用户可以看到和交互的组件,例如按钮、标签和文本框。
-
控制器:控制器是应用程序中的中介,它负责管理模型和视图之间的通信。控制器还处理用户输入并相应地更新模型和视图。
在Swing中,模型通常表示应用程序中的数据,例如列表中的元素或表格中的单元格。视图通常是Swing组件,例如列表、表格或标签。控制器通常是Swing事件处理程序,例如按钮单击或列表选择事件的处理程序。
使用MVC可以使开发人员更容易维护和扩展应用程序,因为它将应用程序分解成三个独立的组件。例如,如果您想更改应用程序中的数据存储方式,则只需更改模型部分,而不必更改视图或控制器。这使得应用程序更易于维护,并且更容易扩展。
以下是一个Java Swing和模型-视图-控制器设计模式的简单示例,该示例显示一个包含按钮和标签的窗口。当用户单击按钮时,标签中的文本将更改。
模型类:
public class Model {
private String data = "Hello, World!";
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
}
视图类:
public class View extends JFrame {
private JLabel label = new JLabel("Hello, World!");
private JButton button = new JButton("Change Text");
public View() {
super("MVC Example");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new BorderLayout());
add(label, BorderLayout.CENTER);
add(button, BorderLayout.SOUTH);
pack();
setLocationRelativeTo(null);
}
public void setButtonClickListener(ActionListener listener) {
button.addActionListener(listener);
}
public void updateLabelText(String text) {
label.setText(text);
}
}
控制器类:
public class Controller {
private Model model;
private View view;
public Controller(Model model, View view) {
this.model = model;
this.view = view;
view.setButtonClickListener(new ButtonClickListener());
}
class ButtonClickListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
String newData = "MVC Example";
model.setData(newData);
view.updateLabelText(model.getData());
}
}
}
主类:
public class Main {
public static void main(String[] args) {
Model model = new Model();
View view = new View();
Controller controller = new Controller(model, view);
view.setVisible(true);
}
}
这个例子中,我们创建了一个Model类,包含一个字符串数据。我们创建了一个View类,继承自JFrame,包含一个JLabel和一个JButton。我们使用BorderLayout将标签放在中间,将按钮放在南侧。我们还创建了一个Controller类,将Model和View类联系起来。在ButtonClickListener类中,当用户单击按钮时,我们更改模型中的数据,并更新标签文本。
最后,在主类中,我们创建了一个Model对象、一个View对象和一个Controller对象,并显示视图。 当用户单击“更改文本”按钮时,标签中的文本将更改为“MVC Example”。
11.2 布局管理概述
Java中有多种布局管理器可用于界面设计和组件布局。每个布局管理器都具有自己的特点和适用范围,可以根据需要选择使用。
布局管理器的选择取决于需要实现的界面效果和组件布局要求。
11.2.1 布局管理器
常见的布局管理器包括:
-
BorderLayout:将组件布置在区域的边缘上,支持东、西、南、北和中间五个位置。
-
FlowLayout:将组件按照添加的顺序从左到右排列,如果一行放不下则自动换行。
-
GridLayout:将组件按照行和列的方式排列,每个网格的大小相同。
-
GridBagLayout:将组件按照网格的方式排列,但是每个网格的大小可以不同,可以设置每个组件的位置和大小。
-
BoxLayout:将组件按照行或列排列,可以设置组件的对齐方式。
-
CardLayout:将多个组件叠加在同一位置,只显示其中的一个组件,可以通过切换卡片的方式显示不同的组件。
以下是一个使用Java布局管理器的代码实例:
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
public class LayoutManagerDemo extends JFrame {
public LayoutManagerDemo() {
// 设置窗口标题
setTitle("布局管理器示例");
// 设置窗口大小
setSize(400, 200);
// 创建容器
JPanel panel1 = new JPanel();
JPanel panel2 = new JPanel();
JPanel panel3 = new JPanel();
// 设置每个容器使用不同的布局管理器
panel1.setLayout(new BorderLayout());
panel2.setLayout(new FlowLayout());
panel3.setLayout(new GridLayout(2, 2));
// 在每个容器中添加组件
panel1.add(new JButton("北"), BorderLayout.NORTH);
panel1.add(new JButton("南"), BorderLayout.SOUTH);
panel1.add(new JButton("东"), BorderLayout.EAST);
panel1.add(new JButton("西"), BorderLayout.WEST);
panel1.add(new JLabel("中间"), BorderLayout.CENTER);
panel2.add(new JLabel("姓名:"));
panel2.add(new JTextField(10));
panel2.add(new JLabel("年龄:"));
panel2.add(new JTextField(3));
panel3.add(new JButton("1"));
panel3.add(new JButton("2"));
panel3.add(new JButton("3"));
panel3.add(new JButton("4"));
// 将三个容器添加到窗口中
setLayout(new GridLayout(3, 1));
add(panel1);
add(panel2);
add(panel3);
// 显示窗口
setVisible(true);
// 设置窗口关闭时的操作
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
new LayoutManagerDemo();
}
}
上面的代码使用了三种不同的布局管理器,分别是BorderLayout、FlowLayout和GridLayout。运行代码,可以看到窗口中有三个区域,每个区域使用不同的布局管理器来控制组件的位置和大小。
11.2.2边框布局
Java的BorderLayout(边框布局)是一种最基本的布局方式,它将容器分为五个区域:东,南,西,北和中间。
例如,下面是一个简单的将标签放置在边框布局中的Java代码:
import javax.swing.*;
public class BorderLayoutDemo {
public static void main(String[] args) {
JFrame frame = new JFrame("BorderLayout Demo");
frame.setSize(300, 200);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JLabel label1 = new JLabel("North Label");
frame.add(label1, BorderLayout.NORTH);
JLabel label2 = new JLabel("South Label");
frame.add(label2, BorderLayout.SOUTH);
JLabel label3 = new JLabel("East Label");
frame.add(label3, BorderLayout.EAST);
JLabel label4 = new JLabel("West Label");
frame.add(label4, BorderLayout.WEST);
JLabel label5 = new JLabel("Center Label");
frame.add(label5, BorderLayout.CENTER);
frame.setVisible(true);
}
}
在这个例子中,我们创建一个JFrame对象来承载我们的UI,然后使用边框布局将五个标签添加到UI中。每个标签都添加到了不同的区域,其中中央区域由最后一个标签占据。
BorderLayout非常灵活,可以使用许多不同的组件,包括按钮、文本框、下拉框等。您也可以将组件添加到任何区域,或者使用JPanel容器来嵌套布局。
11.3 文本输入
一、 Scanner 类
在 Java 中,可以使用 Scanner 类来实现从控制台或文件中读取文本输入。以下是一个简单的示例代码,演示如何使用 Scanner 类从控制台中读取用户输入的字符串:
import java.util.Scanner;
public class TextInputExample {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in); // 创建 Scanner 对象
System.out.print("请输入您的姓名:");
String name = scanner.nextLine(); // 读取用户输入的一行字符串
System.out.println("您输入的姓名是:" + name);
scanner.close(); // 关闭 Scanner 对象
}
}
在上面的代码中,Scanner(System.in)
创建了一个新的 Scanner 对象,并将其与标准输入流 System.in
进行关联,以便从命令行接收输入。scanner.nextLine()
方法读取用户输入的一行字符串,并将其存储在 name
变量中。
除了 nextLine()
外,Scanner 类还提供了许多其他有用的方法,可以根据需要使用,例如 nextInt()
、nextDouble()
、nextBoolean()
等。可以参考 Java 官方文档来了解更多 Scanner 类的用法和方法。
二、 JTextField 组件
在 Java 中,可以使用 JTextField 组件实现在窗口中接收用户的文本输入。以下是一个简单的示例代码:
import java.awt.FlowLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
public class WindowTextInputExample extends JFrame {
private JTextField textField;
public WindowTextInputExample() {
// 设置窗口标题和布局
super("窗口文本输入示例");
setLayout(new FlowLayout());
// 添加标签和文本框
add(new JLabel("请输入您的姓名:"));
textField = new JTextField(20);
add(textField);
// 设置窗口大小,使其适应布局和组件大小
pack();
// 设置窗口的关闭行为和可见性
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
}
public static void main(String[] args) {
new WindowTextInputExample();
}
}
在上面的代码中,我们创建了一个继承自 JFrame 的子类 WindowTextInputExample,并在其构造函数中添加了一个 JLabel 和一个 JTextField。JTextField 的第一个参数是它的列数,而我们设置为 20。
然后我们设置窗口的布局为 FlowLayout,并将标签和文本框添加到其中。最后,我们使用 pack()
方法使窗口适应布局和组件大小,然后设置窗口的关闭行为和可见性。
运行该示例,可以在窗口中输入文本并将其存储在 textField
变量中,以便进一步处理或显示。
11.3.1 文本域
Java 窗口文本域(JTextArea)是一种多行文本输入组件,它可以在图形用户界面(GUI)应用程序中用于接收用户的文本输入,并在窗口中显示多行文本。与单行文本框不同,Java 窗口文本域可以容纳多行文本,用户可以在其文本区域中输入和编辑多个段落。
在 Java 中,可以使用 JTextArea 类来创建文本域,该类支持多种文本编辑和显示功能,例如文本自动换行、滚动文本、复制和粘贴等。为了在窗口中显示 JTextArea,可以将其放入 JScrollPane 中,以便用户可以滚动文本以便查看和编辑。
Java 窗口文本域通常用于收集用户反馈、显示日志或其他长文本数据,并且可以与其他窗口组件(如标签、按钮)一起使用,以便构建交互式应用程序。
以下是一个简单的示例:
import java.awt.BorderLayout;
import java.awt.Dimension;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.ScrollPaneConstants;
public class WindowTextAreaExample extends JFrame {
private JTextArea textArea;
public WindowTextAreaExample() {
// 设置窗口标题和布局
super("窗口文本域示例");
setLayout(new BorderLayout());
// 添加标签和文本域
add(new JLabel("请输入您的意见或建议:"), BorderLayout.NORTH);
textArea = new JTextArea(10, 30);
textArea.setLineWrap(true);
textArea.setWrapStyleWord(true);
JScrollPane scrollPane = new JScrollPane(textArea);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
add(scrollPane, BorderLayout.CENTER);
// 设置窗口大小和可见性
setSize(new Dimension(400, 300));
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
}
public static void main(String[] args) {
new WindowTextAreaExample();
}
}
在上面的代码中,我们创建了一个继承自 JFrame 的子类 WindowTextAreaExample,并在其构造函数中添加了一个 JLabel 和一个 JTextArea。我们设置 JTextArea 的行数和列数,以便它可以显示多行文本,然后使用 setLineWrap(true)
和 setWrapStyleWord(true)
方法启用文本自动换行。接着,我们将 JTextArea 放入 JScrollPane 中,以便用户可以滚动文本以便查看和编辑。
最后,我们设置了窗口的大小和关闭行为,并使其可见。
在运行该示例时,你会看到一个带有标签和文本域的窗口,在文本域中可以输入和编辑多行文本。
11.3.2 标签与标签组件
Java 标签是一种组件,用于在图形用户界面(GUI)应用程序中显示文本或图像,并为用户提供有关应用程序中其他组件的信息。Java 标签通常用于标识窗口中的其他组件,例如按钮、文本框或文本域,并为用户提供与这些组件相关的信息。Java 中的标签组件(JLabel)可以通过编程方式创建和设置。
Java 标签组件具有多种属性和方法,可以用于控制标签的外观、位置和显示内容。例如,可以设置标签的文本、字体、大小和颜色,并指定标签的位置和对齐方式。标签组件还可以与其他 GUI 组件一起使用来实现各种布局和设计效果。
在 Java 中,可以通过以下代码创建标签:
JLabel label = new JLabel("Hello World");
此代码将创建一个标签,其中包含文本“Hello World”。可以将此标签添加到 GUI 应用程序中的其他容器中,例如 JFrame、JPanel 或 JDialog。
要设置标签组件的其他属性,请使用标签组件的方法或属性访问器。例如,要更改标签的文本颜色,请使用以下代码:
label.setForeground(Color.RED);
此代码将更改标签的文本颜色为红色。
下面是一个Java标签与标签组件的代码实现,并设置了标签的文本、字体、大小和颜色,指定了标签的位置和对齐方式。
import javax.swing.*;
import java.awt.*;
public class LabelExample extends JFrame {
public LabelExample() {
setTitle("Java Label Example");
setLayout(new FlowLayout());
JLabel label = new JLabel("Hello World", JLabel.CENTER);
label.setFont(new Font("Arial", Font.BOLD, 20));
label.setForeground(Color.BLUE);
add(label);
setSize(300, 200);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
}
public static void main(String[] args) {
new LabelExample();
}
}
在这个例子中,我们创建了一个标签,通过JLabel
类创建,并传入要显示的文本和对齐方式(JLabel.CENTER
表示居中对齐)。接着,我们通过setFont()
方法设置了标签的字体、大小和颜色,这里使用了Arial字体,粗体样式和大小为20pt,颜色为蓝色。最后,我们将标签添加到窗体中,并设置窗体大小、居中位置和关闭操作。运行程序,我们将看到一个带有"Hello World"标签的窗体。
说明:
JLabel 类表示一个简单的文本标签组件。它可以显示单行文本或多行文本,也可以设置字体、大小、颜色等属性。
Font 类表示字体,可以设置字体类型、样式和大小。
Color 类表示颜色,可以设置标签的前景色和背景色。
getContentPane() 方法返回一个容器对象,可以将标签添加到该容器中。
setSize() 方法设置 JFrame 的大小。
setDefaultCloseOperation() 方法设置 JFrame 关闭操作。
setVisible() 方法显示 JFrame。
11.3.3 密码域
Java密码域是一种Swing GUI组件,用于输入密码或其他敏感信息,并以星号或其他字符来代替输入字符。下面是一个Java密码域的示例代码:
import javax.swing.*;
import java.awt.*;
public class PasswordFieldExample extends JFrame {
public PasswordFieldExample() {
setTitle("Java Password Field Example");
setLayout(new FlowLayout());
JLabel label = new JLabel("Enter your password:");
add(label);
JPasswordField passwordField = new JPasswordField(10);
add(passwordField);
setSize(300, 200);
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
}
public static void main(String[] args) {
new PasswordFieldExample();
}
}
在这个例子中,我们创建了一个密码域,通过JPasswordField
类创建,并指定了最大输入字符数为10。接着,我们将密码域添加到窗体中,并设置窗体大小、居中位置和关闭操作。运行程序,我们将看到一个提示用户输入密码的标签和一个用星号隐藏用户输入的密码域。
11.3.4 文本区
Java文本区是一个基于Java平台的用户界面组件,用于显示和编辑文本。Java文本区包含多行文本,可用于输入或输出数据。
Java文本区可用于多种应用程序中,例如文本编辑器、代码编辑器、聊天应用程序、网页编辑器等。
Java文本区支持多种文本格式,包括纯文本、HTML、RTF等。它还支持多种文本操作,如复制、剪切、粘贴、撤销、重做等。
Java文本区的优点包括易于使用、可定制性强、支持多种文本格式和操作,以及适用于各种应用程序。
以下是一个简单的Java文本区实例代码:
import javax.swing.*;
import java.awt.*;
public class TextDemo extends JFrame {
private JTextArea textArea;
public TextDemo() {
setTitle("Java 文本区示例");
setSize(300, 200);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
textArea = new JTextArea();
JScrollPane scrollPane = new JScrollPane(textArea);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
Container contentPane = getContentPane();
contentPane.add(scrollPane, BorderLayout.CENTER);
}
public static void main(String[] args) {
TextDemo demo = new TextDemo();
demo.setVisible(true);
}
}
这个程序创建了一个包含一个文本区的窗口。JTextArea 和 JScrollPane 类用于创建文本区和滚动条。内容面板使用 BorderLayout 布局,将滚动面板添加到 CENTER 区域。调用 setVisible(true) 方法显示窗口。
11.3.5 滚动窗格
Java 中的滚动窗格(ScrollPane)是一种 GUI 组件,通常用来显示包含大量内容的组件或面板。它允许用户使用滚动条来滚动内容,以便能够查看整个内容。以下是使用 Java Swing 创建滚动窗格的示例代码:
import javax.swing.*;
public class ScrollPaneDemo extends JFrame {
public ScrollPaneDemo() {
// 创建一个包含大量内容的面板
JPanel panel = new JPanel();
for (int i = 0; i < 100; i++) {
panel.add(new JLabel("Label " + i));
}
// 创建一个滚动窗格并将面板添加到其中
JScrollPane scrollPane = new JScrollPane(panel);
// 将滚动窗格添加到 JFrame 中
getContentPane().add(scrollPane);
// 设置 JFrame 的属性
setTitle("ScrollPane Demo");
setSize(300, 200);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] args) {
new ScrollPaneDemo();
}
}
在上面的示例中,我们创建一个 JPanel 作为滚动窗格的视口视图,并将其添加到一个 JScrollPane 中。最后,我们将 JScrollPane 添加到 JFrame 中,使用户可以在窗口中滚动面板。
11.4 选择组件
Java提供了许多选择组件,常用的有下拉列表框、单选按钮、复选框、列表框等。
-
下拉列表框(JComboBox):展示一个下拉列表,选中一项后可以获取对应的值。
-
单选按钮(JRadioButton):展示一组单选按钮,只能选中其中一个。
-
复选框(JCheckBox):展示一组复选框,可以选中多个。
-
列表框(JList):展示一个列表,可以选中多个或者单个。
-
树形列表(JTree):展示一个树形结构,可以展开和收起节点,选中节点获取对应的值。
-
表格(JTable):展示一个表格,可以编辑单元格和选中行获取对应的数据。
通过设置监听器,可以在用户进行选择时触发相应的事件,实现数据的处理和交互。
11.4.1 复选框
Java中的复选框(checkbox)是一种常见的用户界面组件,它允许用户选择一个或多个选项。以下是一个简单的Java复选框的例子:
import javax.swing.*;
import java.awt.*;
public class CheckboxExample extends JFrame {
CheckboxExample() {
setTitle("Checkbox Example");
// 创建多个复选框
Checkbox checkbox1 = new Checkbox("Option 1");
Checkbox checkbox2 = new Checkbox("Option 2");
Checkbox checkbox3 = new Checkbox("Option 3");
// 添加复选框到面板
JPanel panel = new JPanel();
panel.add(checkbox1);
panel.add(checkbox2);
panel.add(checkbox3);
// 添加面板到窗口并设置大小
getContentPane().add(panel);
setSize(300, 200);
setVisible(true);
}
public static void main(String[] args) {
CheckboxExample checkboxExample = new CheckboxExample();
}
}
在这个例子中,我们创建了三个复选框并将它们添加到一个面板中。然后将面板添加到窗口中以显示它们。运行这个程序,你会看到一个包含三个复选框的窗口。
如果用户选中了一个复选框,可以使用isSelected()
方法来检查它是否被选中。例如:
if (checkbox1.isSelected()) {
// 处理复选框1被选中的情况
}
使用监听器,可以通过监听复选框的状态变化来触发相应的代码。例如,使用ItemListener
监听器:
checkbox1.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent e) {
// 检查复选框1是否选中
if (checkbox1.isSelected()) {
// 处理复选框1被选中的情况
} else {
// 处理复选框1被取消选中的情况
}
}
});
Java 复选框的API
API 名称 | 描述 |
---|---|
JCheckBox() | 创建一个未选中的复选框 |
JCheckBox(String text) | 创建一个未选中的复选框,并设置文本 |
JCheckBox(String text, boolean selected) | 创建一个指定选中状态的复选框,并设置文本 |
isSelected() | 返回复选框的选中状态 |
setSelected(boolean selected) | 设置复选框的选中状态 |
getText() | 返回复选框的文本 |
setText(String text) | 设置复选框的文本 |
setMnemonic(int mnemonic) | 设置复选框的助记符 |
addItemListener(ItemListener l) | 添加一个项目监听器 |
removeItemListener(ItemListener l) | 移除一个项目监听器 |
getAccessibleContext() | 返回此组件的可访问上下文 |
11.4.2 单选按钮
Java 单选按钮是一种图形用户界面元素,用于让用户在多个可选项中选择一个。它通常与其他选项按钮一起使用,如复选框和下拉框,以提供一组可选项。
在 Java 中,可以使用 JRadioButton 类来创建单选按钮。要创建一个单选按钮,首先需要创建一个 ButtonGroup 对象,然后创建多个 JRadioButton 对象,并将它们添加到 ButtonGroup 中。这将确保在同一时间内只有一个按钮可以被选中。最后,将单选按钮添加到容器中以显示它们。
以下是一个示例代码,使用 Java 中的单选按钮:
import javax.swing.ButtonGroup;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
public class RadioButtonExample extends JFrame {
JPanel panel;
JRadioButton option1, option2, option3;
public RadioButtonExample() {
setTitle("Radio Button Example");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(300, 200);
panel = new JPanel();
ButtonGroup group = new ButtonGroup();
option1 = new JRadioButton("Option 1");
option2 = new JRadioButton("Option 2");
option3 = new JRadioButton("Option 3");
group.add(option1);
group.add(option2);
group.add(option3);
panel.add(option1);
panel.add(option2);
panel.add(option3);
add(panel);
setVisible(true);
}
public static void main(String[] args) {
new RadioButtonExample();
}
}
Java 单选按钮的所有API
API名称 | 描述 |
---|---|
JRadioButton() | 创建一个没有标签的单选按钮 |
JRadioButton(String text) | 创建一个带标签的单选按钮,标签为指定的文本 |
JRadioButton(String text, boolean selected) | 创建一个带标签的单选按钮,标签为指定的文本,初始的选择状态由selected指定 |
void addActionListener(ActionListener listener) | 向单选按钮添加一个ActionListener,当用户点击按钮时调用 |
void setSelected(boolean selected) | 设置此单选按钮的选中状态,选中为true,未选中为false |
boolean isSelected() | 返回此单选按钮的选中状态 |
ButtonModel getModel() | 返回此单选按钮的ButtonModel对象 |
void setModel(ButtonModel model) | 分配此单选按钮使用的ButtonModel对象 |
void setActionCommand(String command) | 设置此单选按钮生成的ActionEvent的命令字符串 |
String getActionCommand() | 返回此单选按钮生成的ActionEvent的命令字符串 |
void setEnabled(boolean enabled) | 设置此单选按钮的启用状态,启用为true,禁用为false |
boolean isEnabled() | 返回此单选按钮的启用状态 |
void setBorder(Border border) | 设置此单选按钮的边框 |
void setIcon(Icon icon) | 设置此单选按钮的图标 |
void setOpaque(boolean isOpaque) | 设置此单选按钮的不透明度 |
void setPressedIcon(Icon icon) | 设置此单选按钮被按下时显示的图标 |
void setSelectedIcon(Icon icon) | 设置此单选按钮被选中时显示的图标 |
void setRolloverIcon(Icon icon) | 设置此单选按钮悬停时显示的图标 |
void setRolloverSelectedIcon(Icon icon) | 设置此单选按钮被选中且鼠标悬停时显示的图标 |
void setDisabledIcon(Icon icon) | 设置此单选按钮被禁用时显示的图标 |
void setDisabledSelectedIcon(Icon icon) | 设置此单选按钮被禁用且被选中时显示的图标 |
11.4.3 边框
Java 边框可以用来为窗口元素(如面板、按钮和标签等)添加装饰和边框,使其看起来更具有层次感和美观度。Java 中有许多边框可供选择,如线边框、凸起边框、凹陷边框、标题边框等等。
在 Java 中,可以使用 Border 类和其子类来创建边框。一般来说,要为组件添加边框,可以使用 setBorder 方法并将一个 Border 对象传递给它。例如,为一个 JPanel 添加线边框的代码如下所示:
JPanel panel = new JPanel();
panel.setBorder(BorderFactory.createLineBorder(Color.BLACK));
以上代码会将一个黑色的线边框添加到 panel 组件中。
以下是一些常见的 Java 边框类型及其代码示例:
- 线边框
JPanel panel = new JPanel();
panel.setBorder(BorderFactory.createLineBorder(Color.BLACK));
- 凸起边框
JPanel panel = new JPanel();
panel.setBorder(BorderFactory.createRaisedBevelBorder());
- 凹陷边框
JPanel panel = new JPanel();
panel.setBorder(BorderFactory.createLoweredBevelBorder());
- 标题边框
JPanel panel = new JPanel();
panel.setBorder(BorderFactory.createTitledBorder("My Title"));
以上代码将在 panel 组件周围添加一个带有标题的边框,标题为 “My Title”。
注意,以上示例中的边框类型都属于 BorderFactory 类的静态方法,可直接使用。同时,还可以通过继承 Border 类及其子类,自定义更多的边框类型。
Java 边框的所有API
API名称 | 描述 |
---|---|
JFrame | 创建顶层容器框架,并添加组件 |
JPanel | 创建容器面板 |
BorderLayout | 创建边框布局管理器 |
setLayout | 设置组件的布局管理器 |
setBorder | 设置组件的边框 |
LineBorder | 创建直线边框 |
MatteBorder | 创建带颜色填充的边框 |
EtchedBorder | 创建凹陷风格的边框 |
BevelBorder | 创建斜面边框 |
TitledBorder | 创建带标题的边框 |
EmptyBorder | 创建无边框的边框 |
11.4.4 组合框
Java 组合框(JComboBox)是一个可以显示一个下拉列表的组件,用户可以从下拉列表中选择一个选项。组合框由两部分组成:文本框和下拉箭头按钮。当用户单击下拉箭头按钮时,下拉列表会展开,用户可以通过点击列表中的选项来选择其中一个选项。当选择完成后,下拉列表会自动关闭。
以下是一个简单的 Java 组合框示例:
import javax.swing.*;
import java.awt.*;
public class ComboBoxDemo extends JFrame {
public ComboBoxDemo() {
setTitle("Java ComboBox Demo");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
String[] fruits = {"Apple", "Banana", "Orange", "Pear", "Peach"};
JComboBox<String> comboBox = new JComboBox<>(fruits);
// 将组合框添加到面板中
JPanel panel = new JPanel();
panel.add(comboBox);
add(panel);
// 显示窗口
pack();
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] args) {
new ComboBoxDemo();
}
}
在上面的示例中,我们创建了一个包含多个水果名称的 String 数组,然后将其传递给 JComboBox 构造函数,从而创建了一个组合框。接着,我们将组合框添加到一个 JPanel 中,最后将面板添加到 JFrame 中并显示窗口。
组合框还支持添加事件监听器以响应用户的选择。例如,可以使用 addActionListener 方法为组合框添加一个动作监听器,从而在用户选择某个选项时触发相应的代码。
JComboBox类的API
API名称 | 描述 |
---|---|
addItem(Object item) | 向组合框中添加一个对象 |
removeItem(Object item) | 从组合框中移除指定的对象 |
removeItemAt(int index) | 从组合框中移除指定索引处的对象 |
removeAllItems() | 移除组合框中的所有对象 |
setSelectedItem(Object item) | 设置组合框选中的对象 |
getSelectedItem() | 获取组合框当前选中的对象 |
getSelectedIndex() | 获取组合框当前选中对象的索引 |
setSelectedIndex(int index) | 设置组合框当前选中对象的索引 |
getSelectedObjects() | 获取组合框中所有被选中的对象 |
getItemCount() | 获取组合框中对象的数量 |
getItemAt(int index) | 获取组合框中指定索引处的对象 |
getMaximumRowCount() | 获取组合框下拉列表中显示的最大行数 |
setMaximumRowCount(int count) | 设置组合框下拉列表中显示的最大行数 |
isEditable() | 判断组合框是否可编辑 |
setEditable(boolean b) | 设置组合框是否可编辑 |
getModel() | 获取组合框使用的数据模型 |
setModel(ComboBoxModel model) | 设置组合框使用的数据模型 |
getEditor() | 获取组合框的编辑器组件 |
addActionListener(ActionListener listener) | 向组合框添加动作监听器 |
removeActionListener(ActionListener listener) | 从组合框移除动作监听器 |
11.4.5 滑动条
Java滑动条实例是指一个可以在Java图形用户界面中创建的可滑动的图形控件,它允许用户在一个范围内选择一个数值或数值范围。滑动条通常用于调整应用程序中的参数或值,并提供了一个更直观的方式来进行参数调整。
Java滑动条实例可以用于各种各样的应用程序,例如:音量控制、图像亮度和对比度调整、视频播放进度等。Java提供了一个JSlider类,用于创建滑动条。该类提供了各种方法和属性,可以自定义滑动条的外观和行为。
以下是一个简单的Java滑动条的示例,它创建了一个水平滑动条并在窗口中显示:
import javax.swing.JFrame;
import javax.swing.JSlider;
import javax.swing.JPanel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class SliderExample extends JPanel implements ChangeListener {
private JSlider slider;
public SliderExample() {
slider = new JSlider(JSlider.HORIZONTAL, 0, 100, 50);
slider.addChangeListener(this);
add(slider);
}
public void stateChanged(ChangeEvent e) {
int value = slider.getValue();
System.out.println("当前值为:" + value);
}
public static void main(String[] args) {
JFrame frame = new JFrame("Slider Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(300, 100);
SliderExample panel = new SliderExample();
frame.setContentPane(panel);
frame.setVisible(true);
}
}
该示例创建了一个名为Slider Example的窗口,其中包含一个具有初始值为50的水平滑动条。当用户移动滑块时,程序将显示当前值。您可以使用setMinimum()和setMaximum()方法来设置滑动条的最小值和最大值。此外,您可以使用setMajorTickSpacing()和setMinorTickSpacing()方法设置主要和次要刻度的间距。
滑动条的API
API 名称 | 描述 |
---|---|
JSlider() | 创建一个默认值为0、最小值为0、最大值为100、方向为水平的滑动条 |
JSlider(int orientation) | 创建一个默认值为0、最小值为0、最大值为100的滑动条,方向可以是水平或垂直 |
JSlider(int min, int max) | 创建一个指定最小值和最大值的滑动条,方向为水平 |
JSlider(int min, int max, int value) | 创建一个指定最小值、最大值和默认值的滑动条,方向为水平 |
JSlider(int orientation, int min, int max, int value) | 创建一个指定方向、最小值、最大值和默认值的滑动条 |
setValue(int value) | 设置滑动条的当前值 |
getValue() | 获取滑动条的当前值 |
setMinimum(int minimum) | 设置滑动条的最小值 |
getMinimum() | 获取滑动条的最小值 |
setMaximum(int maximum) | 设置滑动条的最大值 |
getMaximum() | 获取滑动条的最大值 |
setMajorTickSpacing(int spacing) | 设置滑动条上主要刻度的间距 |
setMinorTickSpacing(int spacing) | 设置滑动条上次要刻度的间距 |
setPaintTicks(boolean paintTicks) | 设置是否绘制刻度线 |
setPaintLabels(boolean paintLabels) | 设置是否绘制刻度值标签 |
addChangeListener(ChangeListener listener) | 添加一个变化监听器 |
removeChangeListener(ChangeListener listener) | 移除一个变化监听器 |
getAccessibleContext() | 返回此组件的可访问上下文 |
11.5 菜单
Java菜单是一种在Java图形用户界面中创建的图形控件,用于显示应用程序的菜单项和子菜单,用户可以通过单击菜单项来执行应用程序的不同功能。Java提供了多种菜单类,包括JMenu、JMenuBar和JMenuItem等。其中,JMenu类用于创建菜单,JMenuBar类用于创建菜单栏,而JMenuItem类用于创建菜单项。
Java菜单可以用于各种各样的应用程序,例如:文件编辑器中的“文件”菜单、图像编辑器中的“编辑”菜单、视频播放器中的“文件”和“选项”菜单等。Java菜单可以通过各种属性和方法来自定义,如菜单项的文本、图标、快捷键等。此外,Java菜单还支持子菜单和弹出菜单等功能,可以让应用程序更加灵活和易于使用。
以下是一个简单的Java菜单代码实例:
import javax.swing.*;
import java.awt.*;
public class MenuExample {
public static void main(String[] args) {
JFrame frame = new JFrame("Java菜单示例"); // 创建窗口
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 创建菜单栏
JMenuBar menuBar = new JMenuBar();
// 创建菜单
JMenu fileMenu = new JMenu("文件");
JMenu editMenu = new JMenu("编辑");
JMenu helpMenu = new JMenu("帮助");
// 在菜单中添加菜单项
JMenuItem newMenuItem = new JMenuItem("新建");
JMenuItem openMenuItem = new JMenuItem("打开");
JMenuItem saveMenuItem = new JMenuItem("保存");
JMenuItem exitMenuItem = new JMenuItem("退出");
fileMenu.add(newMenuItem);
fileMenu.add(openMenuItem);
fileMenu.add(saveMenuItem);
fileMenu.addSeparator(); // 添加分隔符
fileMenu.add(exitMenuItem);
JMenuItem cutMenuItem = new JMenuItem("剪切");
JMenuItem copyMenuItem = new JMenuItem("复制");
JMenuItem pasteMenuItem = new JMenuItem("粘贴");
editMenu.add(cutMenuItem);
editMenu.add(copyMenuItem);
editMenu.add(pasteMenuItem);
JMenuItem aboutMenuItem = new JMenuItem("关于");
helpMenu.add(aboutMenuItem);
// 将菜单添加到菜单栏
menuBar.add(fileMenu);
menuBar.add(editMenu);
menuBar.add(helpMenu);
// 将菜单栏设置到窗口中
frame.setJMenuBar(menuBar);
// 设置窗口大小并显示
frame.setSize(400, 300);
frame.setVisible(true);
}
}
以上实例创建了一个有三个菜单的菜单栏,包括“文件”、“编辑”和“帮助”菜单。每个菜单都有一些菜单项,例如“新建”、“打开”等。当用户单击菜单项时,该应用程序将执行相应的操作。
Java 菜单 API
菜单 API | 描述 |
---|---|
JMenuBar | 顶层菜单栏 |
JMenu | 普通菜单 |
JPopupMenu | 弹出式菜单 |
JMenuItem | 菜单项 |
JCheckBoxMenuItem | 复选框菜单项 |
JRadioButtonMenuItem | 单选框菜单项 |
JSeparator | 分隔符 |
MenuSelectionManager | 菜单选择管理器 |
MenuElement | 菜单元素 |
MenuKeyListener | 菜单键盘监听器 |
MenuEvent | 菜单事件 |
MenuContainer | 菜单容器 |
PopupMenuListener | 弹出式菜单监听器 |
PopupMenuEvent | 弹出式菜单事件 |
11.5.1 菜单构建
Java 菜单通过组合使用 JMenuBar、JMenu、JMenuItem 等组件来构建。以下是一个简单的 Java 菜单构建示例:
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
public class MenuExample {
public static void main(String[] args) {
JFrame frame = new JFrame("Java Menu Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// 创建菜单栏
JMenuBar menuBar = new JMenuBar();
// 创建菜单
JMenu fileMenu = new JMenu("文件");
// 创建菜单项
JMenuItem newItem = new JMenuItem("新建");
JMenuItem openItem = new JMenuItem("打开");
JMenuItem saveItem = new JMenuItem("保存");
JMenuItem exitItem = new JMenuItem("退出");
// 将菜单项添加到菜单
fileMenu.add(newItem);
fileMenu.add(openItem);
fileMenu.add(saveItem);
fileMenu.addSeparator(); // 添加分隔符
fileMenu.add(exitItem);
// 将菜单添加到菜单栏
menuBar.add(fileMenu);
// 将菜单栏添加到窗口
frame.setJMenuBar(menuBar);
frame.setSize(300, 200);
frame.setVisible(true);
}
}
在这个例子中,我们创建了一个顶级菜单栏 JMenuBar
,然后创建了一个菜单 JMenu
,然后将菜单项 JMenuItem
添加到菜单中。最后,我们将菜单添加到菜单栏中,再将菜单栏添加到 JFrame 窗口中。
11.5.2 菜单项中的图标
在 Java 菜单项中添加图标可以让菜单更加直观和美观。我们可以使用 setIcon()
方法为菜单项添加图标。
以下是一个简单的 Java 菜单项图标示例:
import javax.swing.*;
public class MenuWithIconExample {
public static void main(String[] args) {
JFrame frame = new JFrame("Java Menu Example");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JMenuBar menuBar = new JMenuBar();
JMenu fileMenu = new JMenu("文件");
JMenuItem newItem = new JMenuItem("新建", new ImageIcon("new.png"));
JMenuItem openItem = new JMenuItem("打开", new ImageIcon("open.png"));
JMenuItem saveItem = new JMenuItem("保存", new ImageIcon("save.png"));
JMenuItem exitItem = new JMenuItem("退出", new ImageIcon("exit.png"));
fileMenu.add(newItem);
fileMenu.add(openItem);
fileMenu.add(saveItem);
fileMenu.addSeparator();
fileMenu.add(exitItem);
menuBar.add(fileMenu);
frame.setJMenuBar(menuBar);
frame.setSize(300, 200);
frame.setVisible(true);
}
}
在上面的代码中,我们为每个菜单项添加了一个与其对应的图标,使用 ImageIcon
类来加载图标文件。如果图标文件与代码文件在同一目录下,可以直接使用文件名加载图标文件。
JMenuItem类的API
方法名 | 返回类型 | 描述 |
---|---|---|
addActionListener | void | 向此菜单项添加动作事件侦听器 |
getAccelerator | KeyStroke | 返回此菜单项的加速键 |
getActionCommand | String | 获取此菜单项的动作命令 |
getIcon | Icon | 获取此菜单项的图标 |
getMnemonic | int | 获取此菜单项的助记键 |
getText | String | 获取此菜单项的文本 |
isEnabled | boolean | 返回此菜单项是否可用 |
isSelected | boolean | 返回此菜单项是否被选中 |
removeActionListener | void | 从此菜单项中删除动作事件侦听器 |
setAccelerator | void | 设置此菜单项的加速键 |
setActionCommand | void | 设置此菜单项的动作命令 |
setEnabled | void | 启用或禁用此菜单项 |
setIcon | void | 设置此菜单项的图标 |
setMnemonic | void | 设置此菜单项的助记键 |
setSelected | void | 如果此菜单项支持选择,则选择或取消选择此菜单项 |
setText | void | 设置此菜单项的文本。 |
11.5.3 复选框和单选框按钮菜单项
Java复选框和单选框按钮菜单项是Java Swing用户界面中的重要组件,它们用于提供用户选择的选项。
复选框是一种允许用户选择多个选项的组件,每个选项都有一个对应的复选框,用户可以选择一个或多个复选框。在Java中,可以使用JCheckBox类来创建复选框组件,例如:
JCheckBox checkBox1 = new JCheckBox("Option 1");
JCheckBox checkBox2 = new JCheckBox("Option 2");
单选框是一种只允许用户选择一个选项的组件,每个选项都有一个对应的单选框。在Java中,可以使用JRadioButton类来创建单选框组件,例如:
JRadioButton radioButton1 = new JRadioButton("Option 1");
JRadioButton radioButton2 = new JRadioButton("Option 2");
单选框和复选框通常用于菜单项中,以提供用户选择不同的操作或选项。在Java中,可以使用JCheckBoxMenuItem和JRadioButtonMenuItem类来创建复选框和单选框菜单项,例如:
JCheckBoxMenuItem checkBoxMenuItem1 = new JCheckBoxMenuItem("Option 1");
JRadioButtonMenuItem radioButtonMenuItem1 = new JRadioButtonMenuItem("Option 1");
要使用这些组件,需要将它们添加到容器中,例如:
JMenu menu = new JMenu("Menu");
menu.add(checkBoxMenuItem1);
menu.add(radioButtonMenuItem1);
这样就可以在菜单中添加复选框和单选框菜单项了。
复选框示例代码:
import javax.swing.*;
import java.awt.*;
public class CheckBoxExample extends JFrame {
private JLabel label;
private JCheckBox checkBox1, checkBox2;
public CheckBoxExample() {
super("复选框示例");
setLayout(new FlowLayout());
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
label = new JLabel("选择您喜欢的编程语言");
add(label);
checkBox1 = new JCheckBox("Java");
checkBox2 = new JCheckBox("Python");
add(checkBox1);
add(checkBox2);
setSize(300, 150);
setVisible(true);
}
public static void main(String[] args) {
new CheckBoxExample();
}
}
单选框按钮菜单项示例代码:
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class RadioButtonMenuItemExample extends JFrame implements ActionListener {
private JLabel label;
private JRadioButtonMenuItem menuItem1, menuItem2, menuItem3;
public RadioButtonMenuItemExample() {
super("单选框按钮菜单项示例");
setLayout(new FlowLayout());
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
label = new JLabel("请选择您的操作系统");
add(label);
ButtonGroup group = new ButtonGroup();
menuItem1 = new JRadioButtonMenuItem("Windows");
menuItem2 = new JRadioButtonMenuItem("Mac");
menuItem3 = new JRadioButtonMenuItem("Linux");
menuItem1.addActionListener(this);
menuItem2.addActionListener(this);
menuItem3.addActionListener(this);
group.add(menuItem1);
group.add(menuItem2);
group.add(menuItem3);
JMenu menu = new JMenu("操作系统");
menu.add(menuItem1);
menu.add(menuItem2);
menu.add(menuItem3);
JMenuBar menuBar = new JMenuBar();
menuBar.add(menu);
setJMenuBar(menuBar);
setSize(300, 150);
setVisible(true);
}
public void actionPerformed(ActionEvent e) {
if (e.getSource() == menuItem1) {
label.setText("您选择了Windows");
} else if (e.getSource() == menuItem2) {
label.setText("您选择了Mac");
} else if (e.getSource() == menuItem3) {
label.setText("您选择了Linux");
}
}
public static void main(String[] args) {
new RadioButtonMenuItemExample();
}
}
11.5.4 弹出菜单
Java 弹出菜单(Popup Menu)是一种在鼠标右键点击时弹出的菜单,通常包含一些常用的操作,方便用户快速操作。在 Java 中,可以使用 JPopupMenu 类来创建弹出菜单。
下面是一个简单的 Java 弹出菜单示例:
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class PopupMenuExample {
public static void main(String[] args) {
JFrame frame = new JFrame("弹出菜单示例");
JPanel panel = new JPanel();
panel.setPreferredSize(new Dimension(400, 300));
// 创建弹出菜单
JPopupMenu popupMenu = new JPopupMenu();
JMenuItem menuItem1 = new JMenuItem("复制");
JMenuItem menuItem2 = new JMenuItem("剪切");
JMenuItem menuItem3 = new JMenuItem("粘贴");
// 将菜单项添加到弹出菜单中
popupMenu.add(menuItem1);
popupMenu.add(menuItem2);
popupMenu.add(menuItem3);
// 向面板添加鼠标右键监听器
panel.addMouseListener(new MouseRightClickListener(popupMenu));
frame.getContentPane().add(panel);
frame.pack();
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
static class MouseRightClickListener extends MouseAdapter {
private JPopupMenu popupMenu;
public MouseRightClickListener(JPopupMenu popupMenu) {
this.popupMenu = popupMenu;
}
@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() == MouseEvent.BUTTON3) { // 判断是否为鼠标右键
popupMenu.show(e.getComponent(), e.getX(), e.getY()); // 显示弹出菜单
}
}
}
}
在上面的示例中,我们创建了一个弹出菜单,然后向面板添加了一个鼠标右键监听器,当用户右键点击时,弹出菜单将出现在鼠标指针所在位置。菜单项的操作可以通过为每个菜单项添加监听器来实现。
JPopupMenu 类的API
方法 | 返回类型 | 描述 |
---|---|---|
JPopupMenu() | 构造 | 创建一个JPopupMenu对象 |
add(component) | void | 添加组件到菜单中 |
add(action) | JMenuItem | 添加一个动作到菜单中 |
addSeparator() | void | 添加一个分隔符到菜单中 |
getComponent(index) | Component | 获取菜单中指定索引的组件 |
getComponentCount() | int | 获取菜单中组件的数量 |
getSubElements() | MenuItem[] | 获取菜单中所有的菜单项和分隔符 |
isVisible() | boolean | 检查菜单是否可见 |
pack() | void | 自动调整菜单大小 |
setInvoker(invoker) | void | 设置菜单的调用者 |
setPopupSize(width, height) | void | 设置菜单的大小 |
setVisible(visible) | void | 设置菜单是否可见 |
方法 | 返回类型 | 描述 |
---|---|---|
JPopupMenu() | 构造 | 创建一个JPopupMenu对象 |
add(component) | void | 添加组件到菜单中 |
add(action) | JMenuItem | 添加一个动作到菜单中 |
addSeparator() | void | 添加一个分隔符到菜单中 |
getComponent(index) | Component | 获取菜单中指定索引的组件 |
getComponentCount() | int | 获取菜单中组件的数量 |
getSubElements() | MenuItem[] | 获取菜单中所有的菜单项和分隔符 |
isVisible() | boolean | 检查菜单是否可见 |
pack() | void | 自动调整菜单大小 |
setInvoker(invoker) | void | 设置菜单的调用者 |
setPopupSize(width, height) | void | 设置菜单的大小 |
setVisible(visible) | void | 设置菜单是否可见 |
11.5.5 键盘助记符和加速器
在Java中,键盘助记符和加速器也是常见的用法,以下是相关的示例代码:
- 使用键盘助记符创建菜单项:
JMenu fileMenu = new JMenu("File");
JMenuItem openItem = new JMenuItem("Open");
openItem.setMnemonic(KeyEvent.VK_O); // 使用‘O’作为键盘助记符
fileMenu.add(openItem);
- 使用加速器创建菜单项:
JMenuItem saveItem = new JMenuItem("Save");
saveItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK)); // 使用Ctrl+S作为加速器
fileMenu.add(saveItem);
- 使用加速器创建按钮:
JButton saveButton = new JButton("Save");
saveButton.setMnemonic(KeyEvent.VK_S);
saveButton.setToolTipText("Save (Ctrl+S)");
saveButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// 处理保存按钮的逻辑
}
});
saveButton.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_MASK), "savePressed");
saveButton.getActionMap().put("savePressed", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
saveButton.doClick();
}
});
在上面的示例中,我们使用了getInputMap()
和getActionMap()
方法来将加速器键绑定到按钮上,并在按钮被按下时模拟Click事件。
11.5.6 启用和禁用菜单选项
启用和禁用菜单选项通常通过以下方法实现:
-
在菜单选项上设置状态:可以设置菜单选项的状态为启用或禁用。以 Java Swing 为例,可以使用 setEnabled(boolean enabled) 方法启用或禁用菜单选项。
-
在事件处理程序中控制状态:可以在事件处理程序中根据需要启用或禁用菜单选项。例如,在处理包含菜单项的窗口的事件时,可以根据窗口状态启用或禁用菜单项。
-
使用条件语句控制状态:可以根据应用程序中的某些条件启用或禁用菜单选项。例如,如果用户未登录,则可以禁用某些菜单选项。
-
通过权限控制状态:可以根据用户的权限启用或禁用菜单选项。例如,在企业级应用程序中,管理员可以访问所有菜单选项,而普通用户只能访问部分菜单选项。
以下是 Java Swing 中启用和禁用菜单选项的示例代码:
import javax.swing.*;
public class MenuExample extends JFrame {
public MenuExample() {
JMenuBar menuBar = new JMenuBar();
JMenu fileMenu = new JMenu("File");
JMenuItem newMenuItem = new JMenuItem("New");
JMenuItem saveMenuItem = new JMenuItem("Save");
JMenuItem exitMenuItem = new JMenuItem("Exit");
// 禁用菜单项
saveMenuItem.setEnabled(false);
// 添加菜单项到菜单
fileMenu.add(newMenuItem);
fileMenu.add(saveMenuItem);
fileMenu.addSeparator();
fileMenu.add(exitMenuItem);
// 添加菜单到菜单栏
menuBar.add(fileMenu);
// 设置菜单栏
setJMenuBar(menuBar);
setSize(300, 200);
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setVisible(true);
}
public static void main(String[] args) {
new MenuExample();
}
}
在上面的示例代码中,我们创建了一个简单的菜单栏,包含一个名为 “File” 的菜单和三个菜单项 “New”、“Save” 和 “Exit”。我们通过调用 setEnabled(false)
方法来禁用了 “Save” 菜单项。你可以尝试更改菜单项的启用状态来了解其不同的行为。
11.5.7 工具条
Java 工具条是界面元素,它是一组按钮、文本区域、标签等常见组件的容器。它通常位于窗体的顶部,可用于快速访问常用功能或命令。
以下是创建和添加工具条到 JFrame 窗体的示例代码:
import javax.swing.*;
import java.awt.*;
public class ToolbarExample extends JFrame {
public ToolbarExample() {
// 创建工具条
JToolBar toolbar = new JToolBar();
// 添加按钮
toolbar.add(new JButton("New"));
toolbar.add(new JButton("Open"));
toolbar.addSeparator();
toolbar.add(new JButton("Save"));
toolbar.add(new JButton("Print"));
// 设置工具条
add(toolbar, BorderLayout.NORTH);
setSize(300, 200);
setLocationRelativeTo(null);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setVisible(true);
}
public static void main(String[] args) {
new ToolbarExample();
}
}
在上面的示例代码中,我们创建了一个简单的工具条,并将其添加到 JFrame 窗体的顶部。工具条包含 New、Open、Save 和 Print 四个按钮。可以调整按钮的布局方式和顺序,也可以向工具条中添加其他组件。
请注意,工具条也可以在应用程序中使用菜单项和其他组件。例如,工具条可以包含一个下拉菜单或文本框,等等。
JtoolBar 类的API
API名称 | 描述 |
---|---|
add | 在工具栏上添加一个组件 |
addSeparator | 在工具栏上添加一个分隔符 |
getAccessibleContext | 返回与此工具栏关联的AccessibleContext |
getMargin | 获取工具栏组件之间的空白边距 |
getOrientation | 获取工具栏的方向 |
getUI | 返回此工具栏的当前UI |
getUIClassID | 返回此类的字符串标识符 |
isBorderPainted | 返回工具栏是否绘制边框 |
isFloatable | 返回工具栏是否可浮动 |
isRollover | 返回鼠标指针是否位于工具栏上 |
setBorderPainted | 设置工具栏是否绘制边框 |
setFloatable | 设置工具栏是否可浮动 |
setMargin | 设置工具栏组件之间的空白边距 |
setOrientation | 设置工具栏的方向 |
setRollover | 设置工具栏的鼠标指针的状态 |
setUI | 为此工具栏设置UI |
updateUI | 为此组件设置UI。 |
11.5.8 工具提示
Java Swing 工具提示是一种可以显示在组件附近的简短、文本形式的消息,用于向用户提供额外的信息或解释。一般情况下,工具提示会在鼠标悬停在该组件上时出现,以指示组件的用途或相关信息。
Java Swing中使用工具提示可以通过以下步骤实现:
- 使用 setToolTipText() 方法为需要显示工具提示的组件设置文本,该方法接受一个字符串作为参数。
- 将该组件添加到需要显示工具提示的容器中(如 JFrame、JPanel、JToolBar 等)。
下面是一个示例代码片段,展示如何在 Java Swing 中使用工具提示:
import javax.swing.*;
import java.awt.*;
public class TooltipExample extends JFrame {
public TooltipExample() {
super("Tooltip Example");
// 创建按钮和标签
JButton button = new JButton("Button");
JLabel label = new JLabel("Label");
// 为按钮和标签设置工具提示
button.setToolTipText("This is a button");
label.setToolTipText("This is a label");
// 创建一个工具栏,并将按钮和标签添加到其中
JToolBar toolBar = new JToolBar();
toolBar.add(button);
toolBar.addSeparator();
toolBar.add(label);
// 将工具栏添加到框架中
add(toolBar, BorderLayout.NORTH);
// 设置框架大小和可见性
setSize(300, 200);
setVisible(true);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public static void main(String[] args) {
new TooltipExample();
}
}
运行该示例代码会显示一个带有工具提示的工具栏,当鼠标悬停在按钮或标签上时,将会显示相应的工具提示。
11.6 复杂布局管理
Java中的复杂布局管理指的是使用一些高级布局管理器,将多个组件以一定的规则、大小和位置摆放在窗口或面板上。
在Java中,常见的复杂布局管理器有:
GridBagLayout:使用表格布局的方式,可以自定义每个单元格的大小和位置,非常灵活,但较为复杂。
SpringLayout:使用连线约束的方式,可以将多个组件连接起来,控制它们的相对位置和大小。
GroupLayout:使用分层次的方式,将组件按照层次关系进行布局,可以快速实现复杂的布局。
CardLayout:可以将多个面板叠放在一起,只显示其中的一个,并在需要时切换面板。
11.6.1 网格包布局
网格包布局(GridBagLayout)是Java Swing中最灵活和功能最强大的布局管理器之一,它可以使用表格形式来排列组件,可以在单元格中设置组件的大小、位置和对齐方式等,可以实现复杂的布局。
使用网格包布局需要使用GridBagConstraints类,该类可以设置组件在容器中的位置和大小、对齐方式等属性。
下面是一个简单的代码示例,演示如何使用网格包布局管理器排列一个文本框和一个按钮:
import javax.swing.*;
import java.awt.*;
public class GridBagLayoutDemo extends JFrame {
public GridBagLayoutDemo() {
super("GridBagLayout Demo");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new GridBagLayout());
// 创建一个文本框
JTextField textField = new JTextField("这是一个文本框");
textField.setPreferredSize(new Dimension(200, 30));
// 创建一个按钮
JButton button = new JButton("这是一个按钮");
button.setPreferredSize(new Dimension(100, 30));
// 创建一个GridBagConstraints对象
GridBagConstraints constraints = new GridBagConstraints();
// 设置文本框在第一行第一列,占据1列,宽度为1,高度为1,水平和垂直方向都居中对齐
constraints.gridx = 0;
constraints.gridy = 0;
constraints.gridwidth = 1;
constraints.gridheight = 1;
constraints.fill = GridBagConstraints.BOTH;
constraints.anchor = GridBagConstraints.CENTER;
add(textField, constraints);
// 设置按钮在第二行第一列,占据1列,宽度为1,高度为1,水平和垂直方向都居中对齐
constraints.gridx = 0;
constraints.gridy = 1;
add(button, constraints);
// 设置窗口大小和位置
setSize(400, 200);
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] args) {
new GridBagLayoutDemo();
}
}
以下是GridBagConstraints类的API
方法名 | 描述 |
---|---|
public GridBagConstraints() | 创建一个具有默认值的网格包约束对象 |
public int getAnchor() | 返回组件在网格包中的位置锚点 |
public void setAnchor(int anchor) | 设置组件在网格包中的位置锚点 |
public int getFill() | 返回组件在其网格单元格中的填充方式 |
public void setFill(int fill) | 设置组件在其网格单元格中的填充方式 |
public int getGridheight() | 返回组件在网格包中所占据的行数 |
public void setGridheight(int gridheight) | 设置组件在网格包中所占据的行数 |
public int getGridwidth() | 返回组件在网格包中所占据的列数 |
public void setGridwidth(int gridwidth) | 设置组件在网格包中所占据的列数 |
public int getGridx() | 返回组件在网格包中的列索引 |
public void setGridx(int gridx) | 设置组件在网格包中的列索引 |
public int getGridy() | 返回组件在网格包中的行索引 |
public void setGridy(int gridy) | 设置组件在网格包中的行索引 |
public double getWeightx() | 返回组件在水平方向上所占据的额外空间权重 |
public void setWeightx(double weightx) | 设置组件在水平方向上所占据的额外空间权重 |
public double getWeighty() | 返回组件在垂直方向上所占据的额外空间权重 |
public void setWeighty(double weighty) | 设置组件在垂直方向上所占据的额外空间权重 |
public int getIpadx() | 返回组件在水平方向上所需的额外填充 |
public void setIpadx(int ipadx) | 设置组件在水平方向上所需的额外填充 |
public int getIpady() | 返回组件在垂直方向上所需的额外填充 |
public void setIpady(int ipady) | 设置组件在垂直方向上所需的额外填充 |
一、增量字段
增量字段(也称为调整器)是Swing GUI库中可用的一个组件,它允许用户通过增加或减少一个特定的数值来调整数字。在Java Swing中,可以使用JSpinner类来创建增量字段。
下面是一个简单的Java Swing增量字段示例:
import javax.swing.*;
import java.awt.*;
public class IncrementFieldApp extends JFrame {
private JSpinner spinner;
public IncrementFieldApp() {
super("Increment Field Example");
// 创建增量字段
spinner = new JSpinner(new SpinnerNumberModel(0, 0, 100, 1));
spinner.setPreferredSize(new Dimension(100, 25));
// 添加增量字段到窗口
getContentPane().setLayout(new FlowLayout());
getContentPane().add(spinner);
setSize(200, 100);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setVisible(true);
}
public static void main(String[] args) {
new IncrementFieldApp();
}
}
在上面的示例中,我们使用SpinnerNumberModel类来创建一个新的增量字段。它需要四个参数:
- 初始值(0)
- 最小值(0)
- 最大值(100)
- 步长(1)
我们还设置了增量字段的首选大小,并将其添加到窗口的内容面板中。
当运行此示例时,它将显示一个增量字段,用户可以使用向上或向下箭头按钮来增加或减少数字。您还可以在终端中使用以下命令来运行此示例:
javac IncrementFieldApp.java
java IncrementFieldApp
二、fill和anchor
在Java Swing中,fill和anchor都是布局管理器中的选项。
fill指定了如何填充容器组件。它有四个选项:NONE、HORIZONTAL、VERTICAL和BOTH。
- NONE:组件不会填充容器。
- HORIZONTAL:组件将填充水平空间,但不会填充垂直空间。
- VERTICAL:组件将填充垂直空间,但不会填充水平空间。
- BOTH:组件将填充所有可用的水平和垂直空间。
anchor指定了组件在容器组件中的位置。它有9个选项:NORTH、NORTHWEST、NORTHEAST、WEST、EAST、SOUTH、SOUTHWEST、SOUTHEAST和CENTER。
- NORTH:组件在容器的顶部中心。
- NORTHWEST:组件在容器的左上角。
- NORTHEAST:组件在容器的右上角。
- WEST:组件在容器的左侧中心。
- EAST:组件在容器的右侧中心。
- SOUTH:组件在容器的底部中心。
- SOUTHWEST:组件在容器的左下角。
- SOUTHEAST:组件在容器的右下角。
- CENTER:组件在容器的中心。
这些选项可以用来控制组件在容器中的位置和大小。例如,可以将一个按钮设置为“HORIZONTAL”和“CENTER”,这样它将填充可用的水平空间,并在容器的中心垂直对齐。
三、填充(padding)
在Java Swing中,填充(padding)通常是指在容器中的组件周围添加空白空间。这可以通过设置组件的边框(border)或使用布局管理器中的insets(或padding)属性来实现。
使用边框:
使用边框可以在组件周围添加填充。可以使用JComponent类中的setBorder()方法设置边框。例如,可以使用EmptyBorder类来添加固定大小的填充:
JButton button = new JButton("Click me!");
button.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
这将在按钮周围添加10个像素的填充。EmptyBorder类的构造函数需要四个参数:上、左、下、右边距。
使用布局管理器:
使用布局管理器可以在容器中的组件周围添加填充。在布局管理器中,可以使用insets(或padding)属性来设置填充。例如,可以在GridBagLayout中使用GridBagConstraints类来设置填充:
JPanel panel = new JPanel(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(10, 10, 10, 10);
JButton button = new JButton("Click me!");
panel.add(button, c);
这将在按钮周围添加10个像素的填充。GridBagConstraints类的insets属性需要四个参数:上、左、下、右边距。
四、 网格包布局技巧
以下是一些网格包布局技巧:
- 使用GridBagConstraints来控制组件的布局
GridBagConstraints类是网格包布局的核心。通过设置其属性,可以控制组件的位置、大小和边距。例如,可以使用gridx和gridy属性来指定组件的行和列,使用weightx和weighty属性来指定组件的大小和填充。
- 使用fill属性进行布局
fill属性可以控制组件在其可用空间内的分配方式。例如,可以将fill属性设置为GridBagConstraints.HORIZONTAL,以使组件在水平方向上填充其可用空间。类似地,可以将fill属性设置为GridBagConstraints.VERTICAL或GridBagConstraints.BOTH。
- 使用anchor属性来定位组件
anchor属性可以控制组件在其可用空间内的位置。例如,可以将anchor属性设置为GridBagConstraints.NORTH,以使组件位于其可用空间的顶部。类似地,可以将anchor属性设置为GridBagConstraints.WEST、GridBagConstraints.EAST或GridBagConstraints.SOUTH。
- 使用insets属性来添加填充
insets属性可以为组件周围添加空白空间。例如,可以通过设置insets属性为new Insets(5, 10, 5, 10)来为组件添加上边距5像素,左边距10像素,下边距5像素,右边距10像素的填充。
- 使用gridwidth和gridheight属性来控制组件的大小
gridwidth和gridheight属性可以控制组件在其行和列中占用的单元格数。例如,可以将gridwidth设置为2,以使组件在其所在的列上占用2个单元格。
- 使用Insets来控制布局
Insets是一种网格包布局类,可以为整个容器设置边界和填充。例如,可以创建一个Insets对象,并将其用作容器的insets属性来为整个容器添加填充和边框。
总之,在使用网格包布局时,要注意设置组件的位置、大小、填充和边距。通过使用GridBagConstraints类和其他布局技巧,可以创建各种不同的布局,以满足您的需要。
11.6.2 定制布局管理器
Java Swing提供了多种布局管理器,包括BorderLayout、FlowLayout、GridLayout、CardLayout、SpringLayout等。如果这些布局管理器不能满足需求,也可以自定义布局管理器。
要自定义布局管理器,需要实现LayoutManager接口。该接口只有一个方法:void layoutContainer(Container parent)。实现该方法时,需要根据容器的大小和子组件的需求位置进行布局。
以下是一个简单的自定义布局管理器的示例代码:
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.LayoutManager;
public class CustomLayout implements LayoutManager {
private static final int HORIZONTAL_GAP = 10;
private static final int VERTICAL_GAP = 10;
@Override
public void addLayoutComponent(String name, Component comp) {
// no-op
}
@Override
public void removeLayoutComponent(Component comp) {
// no-op
}
@Override
public Dimension preferredLayoutSize(Container parent) {
Dimension size = new Dimension(0, 0);
int count = parent.getComponentCount();
for (int i = 0; i < count; i++) {
Component component = parent.getComponent(i);
Dimension preferredSize = component.getPreferredSize();
size.width = Math.max(size.width, preferredSize.width);
size.height = Math.max(size.height, preferredSize.height);
}
int hGap = (count - 1) * HORIZONTAL_GAP;
int vGap = (count - 1) * VERTICAL_GAP;
size.width += hGap;
size.height += vGap;
return size;
}
@Override
public Dimension minimumLayoutSize(Container parent) {
return preferredLayoutSize(parent);
}
@Override
public void layoutContainer(Container parent) {
int x = 0;
int y = 0;
int count = parent.getComponentCount();
for (int i = 0; i < count; i++) {
Component component = parent.getComponent(i);
Dimension preferredSize = component.getPreferredSize();
component.setBounds(x, y, preferredSize.width, preferredSize.height);
x += preferredSize.width + HORIZONTAL_GAP;
y += preferredSize.height + VERTICAL_GAP;
}
}
}
该布局管理器会将所有子组件按照他们的首选大小依次排列,并在它们之间添加水平和垂直间距。您可以根据需要修改该示例代码来实现自己的布局管理器。
11.7 对话框
Java对话框是一种在Java GUI应用程序中显示消息、警告或收集用户响应的窗口组件。常见的Java对话框包括:
-
消息对话框:用于显示简单消息或信息,通常包含一个OK按钮。
-
输入对话框:用于向用户收集输入,通常包含文本框和OK和Cancel按钮。
-
文件选择对话框:用于向用户请求选择文件或目录。
-
颜色选择对话框:用于向用户请求选择颜色。
Java提供了许多内置类和方法来创建和管理这些对话框,如JOptionPane、JFileChooser、JColorChooser等。可以使用这些类和方法来创建自定义对话框,以满足不同的需求。
下面是一些常用的对话框:
- JOptionPane对话框
JOptionPane是Swing提供的标准对话框,它可以显示消息、警告、错误、提问等信息,并且可以在对话框中放置按钮。以下是一个示例:
String[] options = {"Yes", "No", "Cancel"};
int response = JOptionPane.showOptionDialog(null, "Do you want to continue?", "Confirmation", JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[2]);
if (response == JOptionPane.YES_OPTION) {
// 用户单击了“Yes”按钮
} else if (response == JOptionPane.NO_OPTION) {
// 用户单击了“No”按钮
} else if (response == JOptionPane.CANCEL_OPTION) {
// 用户单击了“Cancel”按钮
} else if (response == JOptionPane.CLOSED_OPTION) {
// 用户关闭了对话框
}
- JFileChooser对话框
JFileChooser可以让用户选择文件或目录,并返回所选的文件或目录。以下是一个示例:
JFileChooser fileChooser = new JFileChooser();
fileChooser.showOpenDialog(null);
File selectedFile = fileChooser.getSelectedFile();
if (selectedFile != null) {
// 用户选择了文件或目录
}
- JColorChooser对话框
JColorChooser可以让用户选择颜色,并返回所选的颜色。以下是一个示例:
Color selectedColor = JColorChooser.showDialog(null, "Choose a color", Color.BLACK);
if (selectedColor != null) {
// 用户选择了颜色
}
- JDialog对话框
JDialog是Swing提供的基本对话框组件,可以自定义对话框的内容、样式和行为。以下是一个示例:
JDialog dialog = new JDialog();
dialog.setTitle("My Dialog");
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
dialog.setSize(200, 100);
// 向对话框添加组件
JLabel label = new JLabel("Hello, World!");
dialog.add(label);
// 显示对话框
dialog.setVisible(true);
11.7.1 选项对话框
选项对话框是Java Swing中常用的对话框之一,它提供了一种让用户从多个选项中选择一个或多个选项的方式,通常用于询问用户在程序中的一些设置选项,或者让用户从多个可选的操作中选择其一。
Java中提供了JOptionPane类来创建选项对话框,可使用showOptionDialog方法来创建对话框,该方法的用法如下:
int option = JOptionPane.showOptionDialog(parentComponent, message, title, optionType, messageType, icon, options, initialValue);
其中各个参数的含义如下:
- parentComponent:对话框的父组件,通常为窗口或框架。
- message:对话框显示的提示信息。
- title:对话框的标题。
- optionType:对话框显示的选项类型。可以使用JOptionPane的常量值,例如JOptionPane.YES_NO_OPTION、JOptionPane.YES_NO_CANCEL_OPTION、JOptionPane.OK_CANCEL_OPTION等。
- messageType:对话框显示的消息类型。可以使用JOptionPane的常量值,例如JOptionPane.PLAIN_MESSAGE、JOptionPane.INFORMATION_MESSAGE、JOptionPane.WARNING_MESSAGE等。
- icon:对话框显示的图标,通常为JOptionPane的常量值,例如JOptionPane.INFORMATION_MESSAGE、JOptionPane.WARNING_MESSAGE等。
- options:对话框中显示的可选的选项,通常为一个字符串数组。
- initialValue:对话框默认选中的选项。
该方法将返回用户所选择的选项的索引,例如如果用户选择了options数组中的第一个选项,则返回值为0。
以下是一个显示选项对话框的示例代码:
String[] options = {"Red", "Green", "Blue", "Yellow"};
int option = JOptionPane.showOptionDialog(frame, "Choose a color:", "Color Selection", JOptionPane.DEFAULT_OPTION, JOptionPane.PLAIN_MESSAGE, null, options, options[0]);
if (option == 0) {
// 用户选择了红色
} else if (option == 1) {
// 用户选择了绿色
} else if (option == 2) {
// 用户选择了蓝色
} else if (option == 3) {
// 用户选择了黄色
}
以上代码将显示一个选项对话框,让用户从“Red”、“Green”、“Blue”和“Yellow”四个选项中选择一个,用户选择后程序将根据用户所选的选项执行不同的操作。
11.7.2 创建对话框
要在Java Swing中创建对话框,您需要使用JOptionPane类。该类提供了许多静态方法,可用于创建常见类型的对话框,例如消息对话框,确认对话框和输入对话框。
以下是一个简单的示例,演示如何创建一个消息对话框:
import javax.swing.*;
public class DialogExample {
public static void main(String[] args) {
JOptionPane.showMessageDialog(null, "Hello World!");
}
}
在上面的示例中,showMessageDialog()方法使用两个参数调用。第一个参数是对话框的父组件,这里我们使用null来表示没有父组件。第二个参数是要在对话框中显示的消息。
您可以使用类似的方式创建确认对话框,确定对话框和输入对话框。例如,要创建一个确认对话框,您可以使用以下代码:
import javax.swing.*;
public class DialogExample2 {
public static void main(String[] args) {
int result = JOptionPane.showConfirmDialog(null, "Are you sure you want to quit?", "Confirm Quit", JOptionPane.YES_NO_OPTION);
if (result == JOptionPane.YES_OPTION) {
System.exit(0);
}
}
}
在上面的示例中,showConfirmDialog()方法使用三个参数调用。第一个参数是对话框的父组件,第二个参数是要在对话框中显示的消息,第三个参数是对话框的标题。JOptionPane.YES_NO_OPTION参数告诉对话框显示一个Yes和No按钮,然后返回用户选择的结果。在此示例中,如果用户选择Yes,程序将调用System.exit()方法退出。
JDialog是Java Swing类库中的一个类,用于创建对话框(Dialog)。它是JFrame的子类,具有类似的属性和方法。JDialog主要用于创建对话框窗口,通常是模态对话框,用于与用户进行交互和获取用户输入。
JDialog有以下几个常用的构造函数:
-
JDialog(JFrame owner, String title, boolean modal): 创建一个模态对话框,指定它的所有者(即JFrame对象),标题和模态属性。
-
JDialog(JFrame owner, boolean modal): 创建一个模态对话框,指定它的所有者和模态属性。
-
JDialog(JFrame owner): 创建一个非模态对话框,指定它的所有者。
JDialog有以下常用方法:
-
setVisible(boolean b): 设置对话框的可见性。
-
setModal(boolean b): 设置对话框是否是模态的。
-
setTitle(String title): 设置对话框的标题。
-
setResizable(boolean resizable): 设置对话框是否可以调整大小。
-
setLocationRelativeTo(Component c): 设置对话框的位置相对于组件c的位置。
-
getContentPane(): 返回对话框的内容面板。
-
setDefaultCloseOperation(int operation): 设置对话框关闭时的操作。
JDialog通常用于实现模态对话框,这种对话框在弹出时会阻塞所有其他窗口的事件处理,直到该对话框被关闭为止。这在需要获取用户输入或显示重要信息时非常有用。
11.7.3 数据交换
在Java窗口应用程序中进行数据交换,可以使用Java Bean和事件监听器实现。
- 使用Java Bean进行数据交换
与上面提到的使用Java Bean进行数据交换原理一样,创建两个窗口类,分别为发送数据的窗口和接收数据的窗口。在发送窗口中,使用Java Bean承载数据,将Java Bean传递给接收窗口,接收窗口再使用Java Bean的getter方法获取数据。
示例代码:
public class SenderWindow {
private DataBean data = new DataBean();
private ReceiverWindow receiver;
public SenderWindow() {
JButton button = new JButton("发送数据");
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// 设置数据
data.setName("John");
data.setAge(25);
// 传递数据
receiver.receiveData(data);
}
});
}
public void setReceiver(ReceiverWindow receiver) {
this.receiver = receiver;
}
}
public class ReceiverWindow {
public void receiveData(DataBean data) {
// 获取数据
String name = data.getName();
int age = data.getAge();
// 显示数据
JOptionPane.showMessageDialog(null, "姓名:" + name + ", 年龄:" + age);
}
}
public class DataBean implements Serializable {
private String name;
private int age;
// getter和setter方法
}
- 使用事件监听器进行数据交换
在发送窗口中,通过事件监听器将数据发送给接收窗口。在接收窗口中,通过事件监听器接收数据。
示例代码:
public class SenderWindow {
private ActionListener listener;
private ReceiverWindow receiver;
public SenderWindow() {
JButton button = new JButton("发送数据");
button.addActionListener(listener);
}
public void setListener(ActionListener listener) {
this.listener = listener;
}
public void sendData(String data) {
// 传递数据
receiver.receiveData(data);
}
}
public class ReceiverWindow {
public ReceiverWindow(ActionListener listener) {
JButton button = new JButton("接收数据");
button.addActionListener(listener);
}
public void receiveData(String data) {
// 显示数据
JOptionPane.showMessageDialog(null, data);
}
}
// 创建发送窗口
SenderWindow sender = new SenderWindow();
// 创建接收窗口并设置监听器
ReceiverWindow receiver = new ReceiverWindow(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
// 获取数据
String data = "Hello World";
// 发送数据
sender.sendData(data);
}
});
// 设置接收窗口
sender.setReceiver(receiver);
以上两种方法都可以实现窗口间的数据交换,具体选择哪种方法可以根据实际需求和代码结构决定。
11.7.4 文件对话框
Java Swing文件对话框(JFileChooser
)用于让用户浏览和选择文件或文件夹。它提供了多个对话框类型,如打开文件、保存文件、选择文件夹等。通过 javax.swing.JFileChooser
类可以创建一个文件对话框。
在创建 JFileChooser
对象后,可以使用以下方法来定制文件对话框的行为和外观:
setDialogTitle(String title)
: 设置对话框的标题。setFileSelectionMode(int mode)
: 设置选择模式,可以是FILES_ONLY
、DIRECTORIES_ONLY
或FILES_AND_DIRECTORIES
。setSelectedFile(File file)
: 设置默认选中的文件或文件夹。setMultiSelectionEnabled(boolean b)
: 允许多选时设置true
。
最常用的方法是 showOpenDialog()
和 showSaveDialog()
。这些方法将阻塞当前线程,直到用户选择文件或关闭对话框为止。你可以使用返回值来确定用户执行的操作:APPROVE_OPTION
表示用户已成功完成操作,CANCEL_OPTION
表示用户已取消操作。
当对话框关闭后,可以使用 getSelectedFile()
方法获取所选文件或文件夹的路径。如果启用了多选,则可以使用 getSelectedFiles()
获取所有所选文件或文件夹的列表。
下面是一个例子,演示了如何使用 JFileChooser
创建打开、保存和选择文件夹对话框:
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class FileDialogExample {
public static void main(String[] args) {
JFrame frame = new JFrame("File Dialog Example");
JButton openButton = new JButton("Open");
openButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JFileChooser fileChooser = new JFileChooser();
int returnValue = fileChooser.showOpenDialog(null);
if (returnValue == JFileChooser.APPROVE_OPTION) {
System.out.println(fileChooser.getSelectedFile().getName());
}
}
});
JButton saveButton = new JButton("Save");
saveButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JFileChooser fileChooser = new JFileChooser();
int returnValue = fileChooser.showSaveDialog(null);
if (returnValue == JFileChooser.APPROVE_OPTION) {
System.out.println(fileChooser.getSelectedFile().getName());
}
}
});
JButton folderButton = new JButton("Select Folder");
folderButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
JFileChooser fileChooser = new JFileChooser();
fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
int returnValue = fileChooser.showOpenDialog(null);
if (returnValue == JFileChooser.APPROVE_OPTION) {
System.out.println(fileChooser.getSelectedFile().getName());
}
}
});
JPanel panel = new JPanel();
panel.add(openButton);
panel.add(saveButton);
panel.add(folderButton);
frame.add(panel);
frame.setSize(300, 100);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
这个示例演示了如何使用 JFileChooser
创建打开、保存和选择文件夹对话框。首先,创建一个 JFileChooser
对象,然后使用 showOpenDialog()
或 showSaveDialog()
方法显示文件对话框。在对话框中选择文件或文件夹后,可以使用 getSelectedFile()
方法获取所选文件或文件夹的路径。
第十二章 并发
Java 并发指的是在 Java 程序中处理并发操作的能力。在并发编程中,多个线程可以同时执行,这可能会导致竞态条件、死锁和资源争用等问题。为了解决这些问题,Java 提供了许多并发编程工具,如线程、锁、原子变量、并发集合等。以下是一些常用的 Java 并发类和接口:
- Thread 类:Java 中用于创建线程的基本类。
- Runnable 接口:定义一个线程具有的基本行为。
- synchronized 关键字:用于保护临界区,以避免多个线程同时访问同一个资源。
- Lock 接口:用于同步多个线程,可以比 synchronized 更灵活地控制锁的行为。
- ReentrantLock 类:实现了 Lock 接口,可以重入锁定一个资源。
- Condition 接口:与 Lock 接口一起使用,用于等待和唤醒线程。
- Semaphore 类:用于控制同时访问某一资源的线程数量。
- CountDownLatch 类:用于在一个或多个线程等待其他线程执行一定操作后再执行。
Java 并发的目标是提高程序的性能和响应能力,并提高代码的可读性和可维护性。然而,并发编程也引入了一些新的挑战,如竞态条件、死锁、饥饿和活锁等。因此,在编写并发程序时,必须小心谨慎,依赖于正确的工具和模式来实现线程安全,并理解并发编程的基本原则和最佳实践。
12.1 什么是线程
线程(Thread)是计算机程序中的一个执行序列,它是操作系统能够进行运算调度的最小单位。通俗地说,线程就是在一个程序中同时执行的多个流程,每个线程都有自己的计数器、栈和状态。
在 Java 中,每个程序都有一个主线程(Main Thread),它是由 JVM 自动创建的。程序可以通过创建其他线程来实现并发执行不同的任务。Java 中的线程是通过 Thread 类来实现的,每个线程都是一个 Thread 对象。
与进程不同的是,一个进程可以包含多个线程,它们共享进程的资源和内存空间。线程之间可以通过共享内存进行通信,这样线程之间可以更快速地交互数据,并且可以避免复制数据和占用过多的资源。
多线程编程可以提高程序的性能和响应能力,可以实现并发执行多个任务。但同时,也带来了一些挑战,如防止竞态条件、避免死锁、提高线程安全性等问题。因此,在编写多线程程序时,需要小心谨慎,正确使用锁、并发集合等工具来确保线程安全。
以下是一个简单的Java线程操作示例代码:
public class Main {
public static void main(String[] args) throws InterruptedException {
// 创建一个计数器线程
Counter counter = new Counter(10);
Thread t1 = new Thread(counter);
// 创建一个打印 Hello 的线程
Thread t2 = new Thread(() -> {
System.out.println("Hello, world!");
});
// 启动线程
t1.start();
t2.start();
// 等待线程执行完成
t1.join();
t2.join();
System.out.println("All done!");
}
private static class Counter implements Runnable {
private int n;
public Counter(int n) {
this.n = n;
}
@Override
public void run() {
for (int i = 0; i < n; i++) {
System.out.println("Count: " + i);
}
}
}
}
以上代码创建了两个线程,一个是计数器,一个是打印 “Hello, world!” 的线程。启动线程后,程序等待两个线程都执行完成后再输出 “All done!”。在线程启动时,可以使用 start()
方法;在线程执行完成后,可以使用 join()
方法等待线程完成。需要注意的是,线程启动后不一定会立即执行,线程调度是由操作系统控制的,因此可能会有一些不确定性。
12.2 线程状态
Java中的线程状态可以分为以下几种:
1. NEW 新建状态:当创建了线程对象但还未调用start()方法时,线程处于新建状态。
2. RUNNABLE 运行状态:线程启动后,开始执行run()方法,线程处于运行状态。
3. BLOCKED 阻塞状态:当线程被阻塞时,如等待锁或同步方法时,线程处于阻塞状态。
4. WAITING 等待状态:当线程执行了wait()方法时,线程就会进入等待状态。
5. TIMED_WAITING 定时等待状态:当线程调用了带有时间参数的wait()方法、sleep()方法或者join()方法时,线程进入定时等待状态。
6. TERMINATED 终止状态:当线程执行完run()方法或者发生异常时,线程处于终止状态。
12.2.1 新建线程
在Java中,可以通过创建Thread类的对象来实现线程的创建。具体步骤如下:
- 定义一个类,实现Runnable接口,并实现run方法。
class MyRunnable implements Runnable {
public void run() {
//线程执行的代码
}
}
- 创建Thread类的对象,将Runnable对象作为构造函数参数传入。
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
- 调用start方法启动线程。
thread.start();
完整的Java新建线程实例代码如下:
public class NewThreadExample {
public static void main(String[] args) {
// 创建一个Runnable对象
Runnable myRunnable = new Runnable() {
public void run() {
System.out.println("MyRunnable running");
}
};
// 创建一个线程,并将Runnable对象传入
Thread thread = new Thread(myRunnable);
// 启动线程
thread.start();
}
}
12.2.2 可运行线程
Java 的可运行线程分为两种:用户线程和守护线程。
用户线程是指提交给 JVM 执行的线程,它们是 JVM 运行的基础。当所有用户线程都执行完毕后,JVM 才会退出。
守护线程是一种特殊的用户线程,它的作用是为其他线程提供服务。守护线程的优先级比较低,当所有用户线程都执行完毕后,JVM 不会等待守护线程执行完毕而是立即退出。JVM 中的垃圾回收线程就是典型的守护线程。
在 Java 中,通过继承 Thread 类或实现 Runnable 接口来创建一个可运行线程。
12.2.3 阻塞和等待线程
Java 中的线程可以处于阻塞状态或等待状态。
阻塞状态是指当线程试图获取某个锁或执行某个 I/O 操作时,在锁或 I/O 操作完成之前,线程会一直被阻塞。当阻塞结束后,线程会重新回到可运行状态。
等待状态是指当线程需要等待另一个线程执行某些操作后才能继续执行时,线程会进入等待状态。可以通过调用 wait()
方法使线程进入等待状态,等待其他线程调用相同对象的 notify()
或 notifyAll()
方法来唤醒它。
另外,还有一种特殊的等待状态,即 sleep()
方法,调用该方法会使线程暂停一段时间,然后自动唤醒。这种等待状态是不可中断的。
阻塞和等待状态都可能导致线程暂停执行,直到某个条件满足后再继续执行。因此,在多线程编程中需要注意避免死锁或永久等待等问题。
下面是 Java 中阻塞和等待线程的实例代码:
阻塞示例:
public class BlockThread implements Runnable {
private Object lock;
public BlockThread(Object lock) {
this.lock = lock;
}
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "获取到了锁");
try {
//模拟阻塞,这里让线程休眠5秒钟
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "执行完毕");
}
}
}
public class Main {
public static void main(String[] args) {
Object lock = new Object();
Runnable blockThread = new BlockThread(lock);
//启动两个线程
new Thread(blockThread, "线程1").start();
new Thread(blockThread, "线程2").start();
}
}
运行结果:
线程1获取到了锁
线程2获取到了锁
线程1执行完毕
线程2执行完毕
说明:线程1和线程2都试图获取 lock 对象的锁,由于锁是排它的,因此只有一个线程能够获取锁并执行。当其中一个线程获取锁并执行时,另一个线程会被阻塞,直到持锁线程执行完毕并释放锁后,阻塞线程才能竞争获取锁。
等待示例:
public class WaitThread implements Runnable {
private Object lock;
public WaitThread(Object lock) {
this.lock = lock;
}
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "获取到了锁");
try {
//让线程等待其他线程调用notify或notifyAll方法唤醒它
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "被唤醒,继续执行");
}
}
}
public class Main {
public static void main(String[] args) {
Object lock = new Object();
Runnable waitThread = new WaitThread(lock);
//启动一个线程
new Thread(waitThread, "线程1").start();
//让主线程休眠1秒钟,以确保线程1先执行,并等待后面的唤醒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
//唤醒线程1
lock.notify();
}
}
}
运行结果:
线程1获取到了锁
线程1被唤醒,继续执行
说明:线程1先获取 lock 对象的锁,并执行到 lock.wait()
方法处时,线程会进入等待状态并释放锁。主线程等待1秒后,获取 lock 对象的锁,并调用 lock.notify()
方法,唤醒线程1并继续执行。
12.2.4 终止线程
Java 中有多种方式终止线程,以下列举了一些常见的方法:
- 使用标志位终止线程
可以使用一个标志位,表示线程是否需要终止,当标志位被设置为 true 时,线程会停止执行并退出。示例代码如下:
public class FlagThread implements Runnable {
private volatile boolean flag = false;
public void run() {
while (!flag) {
System.out.println(Thread.currentThread().getName() + "正在执行");
try {
//模拟线程执行的业务逻辑
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "已经终止");
}
public void stop() {
flag = true;
}
}
public class Main {
public static void main(String[] args) {
FlagThread flagThread = new FlagThread();
Thread thread = new Thread(flagThread);
thread.start();
//模拟执行5秒钟后终止线程
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flagThread.stop();
}
}
在 FlagThread 类中定义了一个 boolean 类型的变量 flag,当 flag 被设置为 true 时,线程会退出。在 run 方法中循环执行业务逻辑,并在每次循环之后检查 flag 变量的值,如果 flag 被设置为 true,则退出循环并终止线程。stop 方法用于设置 flag 变量的值。
- 使用 interrupt 终止线程
可以调用 Thread 的 interrupt 方法来终止线程。在线程中需要使用 Thread.interrupted() 方法来判断线程是否被终止。示例代码如下:
public class InterruptThread implements Runnable {
public void run() {
while (!Thread.interrupted()) {
System.out.println(Thread.currentThread().getName() + "正在执行");
try {
//模拟线程执行的业务逻辑
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(Thread.currentThread().getName() + "被中断,退出线程");
break;
}
}
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new InterruptThread());
thread.start();
//模拟执行5秒钟后终止线程
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
}
在 InterruptThread 类中使用 Thread.interrupted() 方法来判断线程是否被中断,如果被中断,则退出循环并终止线程。在 Main 类中调用 thread.interrupt() 方法来中断线程。
- 使用 stop 方法终止线程
stop 方法是 Thread 类中的一个方法,可以直接调用来终止线程,但是这种方式容易引起线程的资源泄漏,因为 stop 方法会直接杀死线程并释放线程所持有的锁,导致线程执行到一半时被终止,可能会造成数据的不一致。因此,不推荐使用 stop 方法来终止线程。
12.3 线程属性
线程属性是指控制线程行为和特征的一组设置。这些属性控制着线程的优先级、调度策略、堆栈大小、线程名称等等。
以下是一些常见的线程属性:
-
线程优先级:决定了线程在多个就绪线程中的调度优先级。高优先级的线程被调度执行的概率更大。
-
线程调度策略:指定了线程在多个就绪线程中的调度规则,如时间片轮转、优先级调度等。
-
堆栈大小:决定了每个线程可以使用的内存空间大小。
-
线程名称:用于区分不同的线程,方便调试和可读性。
-
守护线程:当所有非守护线程结束后,守护线程也会自动结束,常用于后台任务或服务。
-
线程同步:线程之间的同步和通信方式,例如锁、信号量、管道等。
通过设置合适的线程属性,可以提高程序的并发效率、节省资源,并保证线程安全。
Java 线程属性可以通过 Thread
类的一些方法设置,下面是一些常用的线程属性及设置方法:
- 线程优先级:可以通过
setPriority(int priority)
方法设置线程的优先级,优先级范围为 1-10,默认优先级为 5。
Thread thread = new Thread();
thread.setPriority(Thread.MAX_PRIORITY); // 设置线程优先级为最高
- 线程调度策略:可以通过
setDaemon(boolean on)
方法设置线程是否为守护线程,守护线程会在所有非守护线程结束时自动结束。
Thread thread = new Thread();
thread.setDaemon(true); // 设为守护线程
- 堆栈大小:可以通过
Thread
类的构造方法或者setDefaultUncaughtExceptionHandler
方法设置线程的堆栈大小。
Thread thread = new Thread(null, null, "threadName", 1024 * 1024); // 设置线程的堆栈大小为 1MB
- 线程名称:可以通过
setName(String name)
方法设置线程的名称。
Thread thread = new Thread();
thread.setName("threadName"); // 设置线程名称
- 线程同步:可以使用
synchronized
关键字或者Lock
接口等方式实现线程的同步和通信。
12.3.1 中断线程
Java 中断线程有两种方式:使用 interrupt()
方法和使用 volatile
关键字。下面分别介绍这两种方式。
- 使用
interrupt()
方法
interrupt()
是 Thread
类的一个方法,用于中断线程。调用 interrupt()
方法并不会立即使线程停止,而是将线程的中断标志设置为 true
,需要线程自己去检测中断标志并作出相应的处理。
Thread thread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 线程任务
}
});
thread.start();
// 中断线程
thread.interrupt();
在上面的代码中,我们使用 isInterrupted()
方法检测线程的中断标志,如果中断标志为 true
,则退出循环,从而中断线程。注意,中断线程并不会立即停止线程,而是需要在线程中做特定的处理才能停止线程。
- 使用
volatile
关键字
使用 volatile
关键字可以让线程之间共享变量,从而实现线程的通信。同时,可以将一个 volatile
变量用于控制线程的启停。
public class MyRunnable implements Runnable {
private volatile boolean stop = false;
@Override
public void run() {
while (!stop) {
// 线程任务
}
}
public void stopThread() {
stop = true;
}
}
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
// 停止线程
runnable.stopThread();
在上面的代码中,我们使用一个 volatile
变量 stop
作为线程的控制变量,当 stop
为 true
时,线程停止。然后在 stopThread()
方法中将 stop
变量置为 true
,从而停止线程。
需要注意的是,使用 volatile
变量控制线程停止只适用于简单的线程任务,如果线程任务比较复杂,就需要使用 interrupt()
方法中断线程。
12.3.2 守护线程
Java 中的守护线程(Daemon Thread)是一种特殊的线程,它的作用是为其他线程提供服务。守护线程是一种后台线程,它并不会影响到整个程序的运行,它会在其他所有非守护线程结束之后自动退出。
在 Java 中,通过将一个线程设置为守护线程,来标识这个线程是一个守护线程。守护线程的创建和普通线程的创建方式是一样的,只需要在线程创建后,调用 setDaemon(true)
方法将其设置为守护线程即可。
Java 的守护线程主要用于为其他线程提供服务,例如垃圾回收线程、JIT 编译线程等。由于守护线程的特殊性,Java 虚拟机在执行结束时不会等待守护线程结束,而是直接退出。因此,守护线程常用于执行一些不需要等待的任务或者是进行一些系统服务的调度等。
下面是一个普通线程和一个守护线程的示例代码:
普通线程:
public class NormalThread extends Thread {
public void run() {
try {
for (int i = 0; i < 10; i++) {
System.out.println("NormalThread: " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(String[] args) {
NormalThread normalThread = new NormalThread();
normalThread.start();
}
}
守护线程:
public class DaemonThread extends Thread {
public void run() {
try {
for (int i = 0; i < 10; i++) {
System.out.println("DaemonThread: " + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class Main {
public static void main(String[] args) {
DaemonThread daemonThread = new DaemonThread();
daemonThread.setDaemon(true);
daemonThread.start();
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread exiting...");
}
}
在这个例子中,我们创建了一个名为 NormalThread
的普通线程和一个名为 DaemonThread
的守护线程。在 Main
类的 main
方法中,我们启动了 NormalThread
线程,而 DaemonThread
线程则被设置为守护线程,并在启动之前调用了 setDaemon(true)
方法。
当程序执行时,NormalThread
将会一直执行,直到它完成了任务或者被中断。而 DaemonThread
将会在程序的主线程执行完毕后自动退出。在 Main
类的 main
方法中,我们让主线程休眠了 5 秒钟,然后输出一条信息,表示程序即将退出。由于 DaemonThread
是一个守护线程,它会在主线程退出之后自动退出。
12.3.3 线程名
线程名是指在Java中为一个线程分配的名称,以便于在调试和跟踪代码时识别线程。可以使用Thread.setName()
方法来设置线程名,使用Thread.getName()
方法来获取线程名。默认情况下,Java会为每个线程生成一个唯一的名字,以"Thread-"开头,后面跟一个数字。
12.3.4 未捕获异常的处理器
Java 中的未捕获异常是指没有被 try-catch 块捕获的异常。在这种情况下,程序会抛出一个异常并终止执行。
为了避免这种情况的发生,可以使用 Java 的异常处理机制。比较常见的方法是使用 try-catch 块来捕获异常并处理它们。
另外,Java 还提供了一个未捕获异常的处理器(UncaughtExceptionHandler),它可以在程序抛出未捕获异常时被调用。这个处理器可以让程序在捕获异常失败的情况下仍能继续执行。
要使用未捕获异常的处理器,可以使用 Thread 类的 setUncaughtExceptionHandler 方法来设置一个异常处理器。例如:
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
System.out.println("Uncaught exception detected in thread " + t.getName() + ": " + e.getMessage());
}
});
在这个例子中,我们创建了一个匿名类实现 UncaughtExceptionHandler 接口,并在其中指定了如何处理未捕获异常。然后我们调用了 Thread 类的 setDefaultUncaughtExceptionHandler 方法来设置默认的未捕获异常处理器。
使用这个方法,我们可以在程序抛出未捕获异常时做出适当的响应,例如记录异常信息或者给用户提示错误信息等。
12.3.5 线程优先级
Java 线程优先级指的是线程的执行优先级,优先级越高的线程会在同等条件下(如 CPU 时间片)先执行。Java 线程优先级范围是 1~10,其中 1 为最低优先级,10 为最高优先级,默认为 5。
可以使用 Thread 类的 setPriority() 方法来设置线程的优先级,例如:
Thread t1 = new Thread(() -> {
// 这里是线程 t1 的执行代码
});
t1.setPriority(8); // 设置 t1 的优先级为 8
t1.start();
需要注意的是,优先级并不能保证一定能按照设定的顺序执行,而是表示一个相对的执行顺序。同时,线程优先级的设置也与操作系统和硬件的具体实现有关。
另外,线程优先级的设定也需要考虑到应用程序的实际需求。如果线程优先级过高,会导致其他任务无法得到充分的 CPU 时间,从而影响整个程序的性能。因此,要合理设置线程优先级,并结合其他的调度策略来实现对程序性能的优化。
12.4 同步
Java 中的同步指的是对多个线程之间的共享资源的访问和操作进行协调,以避免出现并发访问的问题,如数据损坏和数据不一致等。常用的同步机制有 synchronized 和 Lock。
- synchronized
synchronized 是 Java 中最基本的同步机制之一,可以用来对代码块或方法进行同步处理。使用 synchronized 时,需要指定一个对象作为锁,对于同一个锁的访问会进行同步,即同一时刻只有一个线程能够获得这个锁,并执行同步操作。
例如,可以使用 synchronized 对一个共享资源进行同步控制:
class Counter {
private int value = 0;
public synchronized void increment() {
value++;
}
public synchronized void decrement() {
value--;
}
public synchronized int getValue() {
return value;
}
}
- Lock
Lock 是 Java 中另一种同步机制,相比 synchronized,Lock 提供了更为灵活的锁机制和更精细的线程语义。Lock 接口定义了加锁和解锁的方法,并且允许在加锁的时候进行中断等操作,可以更加精细地控制线程的访问。
例如,可以使用 Lock 对一个共享资源进行同步控制:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private final Lock lock = new ReentrantLock();
private int value = 0;
public void increment() {
lock.lock();
try {
value++;
} finally {
lock.unlock();
}
}
public void decrement() {
lock.lock();
try {
value--;
} finally {
lock.unlock();
}
}
public int getValue() {
lock.lock();
try {
return value;
} finally {
lock.unlock();
}
}
}
需要注意的是,使用同步机制会造成一定的性能损失,并且过度地使用同步机制会导致性能下降和死锁等问题。因此,在应用程序中使用同步机制时,需要考虑性能和安全等方面的平衡。
12.4.1 竞态条件的一个例子
一个经典的竞态条件的例子是银行账户的并发访问。假设有两个人同时想要从同一个账户A中取出100元。他们同时发出请求,系统会先检查余额是否足够,如果足够,那么账户A中就会减去100元,然后返回一个成功的响应。但是,在多线程并发的情况下,可能会出现以下的情况:
- 线程1和线程2同时检查余额,发现都足够支持取出100元;
- 线程1首先执行取出100元的命令,此时余额为X - 100;
- 线程2接着执行取出100元的命令,此时余额仍然为X,因为前面已经被线程1减去了100元;
- 线程2也执行完后,账户A中的余额变成了X - 100,而不是X - 200。
这样,在竞态条件下,实际上,两个并发的线程都成功取出了100元,但是账户A的余额并没有减少200元,而只减少了100元。这种情况就被称为竞态条件,是多线程编程中需要注意避免的一种常见的问题。
以下是一个简单的Java代码实例,模拟了两个线程同时对同一个银行账户进行取款操作的情况,可能导致出现竞态条件:
public class BankAccountDemo {
private static int balance = 200;
public static void main(String[] args) {
Thread thread1 = new Thread(new WithdrawTask());
Thread thread2 = new Thread(new WithdrawTask());
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Balance: " + balance);
}
private static class WithdrawTask implements Runnable {
@Override
public void run() {
synchronized (BankAccountDemo.class) {
if (balance >= 100) {
balance = balance - 100;
System.out.println("Withdraw 100 yuan, balance: " + balance);
} else {
System.out.println("Balance is not enough!");
}
}
}
}
}
在上面的代码中,有两个线程同时执行取款操作,每个线程取款100元。如果账户余额足够,那么最终的结果应该是balance为0。但是,因为取款操作不是原子操作,所以在并发情况下会出现竞态条件,最终的结果可能小于0或不为0。
为了避免竞态条件,可以使用同步机制,例如使用synchronized关键字,将取款操作变成原子操作:
private static class WithdrawTask implements Runnable {
@Override
public void run() {
synchronized (BankAccountDemo.class) {
if (balance >= 100) {
balance = balance - 100;
System.out.println("Withdraw 100 yuan, balance: " + balance);
} else {
System.out.println("Balance is not enough!");
}
}
}
}
这样,在竞态条件下,就能保证取款操作的原子性,避免了多线程并发导致的问题。
12.4.2 静态条件详解
Java中静态条件是指在程序编译时就已经确定的条件,如final关键字、静态代码块等。
-
final关键字:final关键字用于声明一个常量或者一个不可变的变量,一旦被赋值就不能再进行修改,使得程序更加安全和稳定。
-
静态代码块:静态代码块是在类被加载时执行的一段代码块,用于进行类的初始化操作。它是在静态变量初始化之后、构造器执行之前执行,可以在其中加载类所需的配置文件、初始化静态变量等。
-
静态方法:静态方法是在类的静态条件下执行的方法,不需要实例化对象即可调用。它通常用于实现工具方法,在程序运行时节省内存和时间。
静态条件的特点是:只要在程序编译阶段确定,不需要实例化对象即可访问,且其值一旦确定就不可更改。因此,在Java中应该尽量使用静态条件来提高程序的性能和安全性。
12.4.3 锁对象
在Java中,锁对象是用来控制并发访问共享资源的一种机制。实现锁对象的方式有以下两种:
-
synchronized关键字:synchronized关键字可以用来修饰方法或代码块,将其变为同步方法或同步代码块。在Java中,每个对象都可以作为一个锁对象,而synchronized关键字的锁机制是基于对象的。
-
Lock接口:JDK5之后引入了Lock接口,提供了比synchronized关键字更为灵活的锁机制。Lock接口的实现类可以实现多种锁机制,例如可重入锁、公平锁、读写锁等。
锁对象的作用是可以让多个线程对共享资源进行访问时,保证同一时刻只能有一个线程进行访问,避免产生并发问题。当一个线程获取到锁对象后,其他线程必须等待该线程释放锁对象后才能继续进行访问。
需要注意的是,在使用锁对象时要避免死锁问题,即两个或多个线程都在等待对方释放锁对象而无法继续进行。为了避免死锁,可以使用tryLock()方法进行非阻塞式获取锁,或者设置超时时间等策略。
以下是一个使用synchronized关键字实现锁对象的示例代码:
public class BankAccount {
private int balance;
public BankAccount(int balance) {
this.balance = balance;
}
// 使用同步方法实现锁对象
public synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
System.out.println(Thread.currentThread().getName() + " withdraw " + amount + " dollars. Remaining balance: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + " withdraw failed. Insufficient balance.");
}
}
public static void main(String[] args) {
BankAccount account = new BankAccount(1000);
// 创建两个线程同时进行取款操作
Thread t1 = new Thread(() -> account.withdraw(600), "Thread1");
Thread t2 = new Thread(() -> account.withdraw(500), "Thread2");
t1.start();
t2.start();
}
}
在上面的示例代码中,使用了synchronized关键字修饰方法,将其变为同步方法。这样,在每个线程执行方法时,会先获取到该方法所属对象的锁对象,然后进行访问。同一时刻只有一个线程能够获取到锁对象,其他线程需要等待该线程释放锁对象后才能继续进行访问。这样就能够保证对共享资源的访问是安全的,避免了并发问题。
12.4.4 条件对象
Java 中的条件对象(Condition Object)是指一个线程对象等待另一个线程对象发出特定信号的机制。它是在 Java 5 中添加到 java.util.concurrent.locks 包中的。
条件对象通常与锁对象一起使用,通过await()方法使线程进入“等待”状态,直到其他线程发出信号(signal/signalAll()方法)通知该线程退出“等待”状态。
下面是一个使用条件对象的示例代码:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionExample {
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
private int counter;
public void increment() {
lock.lock();
try {
counter++;
condition.signalAll();
} finally {
lock.unlock();
}
}
public void decrement() throws InterruptedException {
lock.lock();
try {
while (counter == 0) {
condition.await();
}
counter--;
} finally {
lock.unlock();
}
}
}
在上面的示例中,我们创建了一个条件对象(Condition),该对象与锁对象(Lock)关联。在increment()中,我们增加计数器的值,并使用signalAll()方法通知所有等待线程,计数器的值已更改并且它们可以重新检查它。在decrement()中,我们使用await()方法使线程等待,直到计数器的值大于零。如果计数器的值为0,线程将停止在await()方法处,直到其他线程调用increment()方法更改计数器的值并唤醒等待线程。
条件对象的使用可以帮助开发者避免死锁和其他并发问题,简化代码,并提高应用程序的效率。
12.4.5 synchronized关键字
synchronized是Java中的关键字,可以用于实现同步操作。当synchronized修饰的代码块被执行时,会自动获取对象锁,并在执行结束后自动释放锁。
synchronized可以用于以下三种情况:
-
同步实例方法:使用synchronized修饰实例方法,锁定的是该实例对象。如果多个线程同时访问同一个实例的synchronized方法,则只有一个线程能够执行,其他线程需要等待。
-
同步静态方法:使用synchronized修饰静态方法,锁定的是该类的Class对象。如果多个线程同时访问同一个类的synchronized静态方法,则只有一个线程能够执行,其他线程需要等待。
-
同步代码块:使用synchronized修饰代码块,锁定的是该代码块所在的对象。如果多个线程同时访问同一个对象的synchronized代码块,则只有一个线程能够执行,其他线程需要等待。
使用synchronized可以保证线程安全,但也会带来性能损失,因为只有一个线程能够执行同步代码块,其他线程需要等待。因此,在实现同步时需要权衡性能和线程安全。
以下是使用synchronized关键字实现线程安全的代码示例:
同步实例方法:
public class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
// 在多个线程中共享一个Counter对象,保证线程安全
Counter counter = new Counter();
// 创建并启动多个线程,对Counter对象进行操作
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
// 等待两个线程执行完毕
t1.join();
t2.join();
// 获取计数器的值
int count = counter.getCount();
System.out.println(count); // 输出20000
同步静态方法:
public class Counter {
private static int count = 0;
public static synchronized void increment() {
count++;
}
public static synchronized int getCount() {
return count;
}
}
// 在多个线程中共享Counter类,保证线程安全
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
Counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
Counter.increment();
}
});
t1.start();
t2.start();
// 等待两个线程执行完毕
t1.join();
t2.join();
// 获取计数器的值
int count = Counter.getCount();
System.out.println(count); // 输出20000
同步代码块:
public class Counter {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
public int getCount() {
synchronized (this) {
return count;
}
}
}
// 在多个线程中共享一个Counter对象,保证线程安全
Counter counter = new Counter();
// 创建并启动多个线程,对Counter对象进行操作
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
// 等待两个线程执行完毕
t1.join();
t2.join();
// 获取计数器的值
int count = counter.getCount();
System.out.println(count); // 输出20000
12.4.6 同步块
Java中的同步块指的是使用synchronized
关键字来同步代码块,从而实现多线程的安全访问。
synchronized关键字可以用于同步方法或同步块。使用同步块可以对指定的代码块进行同步,而不是整个方法。同步块的语法格式为:
synchronized (lockObject) {
// 同步代码块
}
其中,lockObject
为锁对象,可以是任何Java对象。当一个线程执行到同步块时,它会尝试获取lockObject
的锁。如果锁对象没有被其它线程占用,则当前线程获取到该锁对象的锁,并执行同步代码块中的代码。当同步块执行完毕后,当前线程会释放该锁对象的锁。
同步块的优点是可以减小同步范围,从而提高并发性能。因为同步方法会锁住整个方法,而同步块只会锁住指定的代码块。如果同步方法中只有小部分代码需要同步,使用同步块可以更加精确地控制同步。
以下是一个使用同步块实现线程安全的计数器类的示例:
public class Counter {
private int count = 0;
private final Object lock = new Object(); // 定义锁对象
public void increment() {
synchronized (lock) { // 使用同步块
count++;
}
}
public int getCount() {
synchronized (lock) { // 使用同步块
return count;
}
}
}
在上述例子中,我们定义了一个lock
对象作为锁对象,然后在increment()
和getCount()
方法中使用同步块来同步代码块,而不是使用synchronized
关键字同步整个方法,这样可以提高并发性能。
需要注意的是,同步块中的对象必须是同一个对象才能实现同步。不同的对象无法实现同步。所以,在使用同步块时,必须选择一个能够被多个线程共享的对象作为锁对象。
12.4.7 监视器概念
Java中的监视器指的是一个对象或类,它提供了一种在多线程环境中同步共享资源的机制。监视器可以用synchronized
关键字来实现,它允许线程在进入同步代码块时获得该对象的锁,从而避免多个线程同时修改共享数据造成的数据不一致问题。
每个Java对象都可以作为一个监视器来使用,当一个线程进入被synchronized
关键字包围的代码块时,它会尝试获取该对象的锁。如果该对象的锁没有被其它线程占用,当前线程将获得锁并可以进入代码块执行操作。如果该对象的锁已经被其它线程占用,则当前线程将阻塞等待锁的释放,直到获得锁为止。
监视器的主要作用是保证在多线程环境中数据的一致性和正确性。当多个线程同时访问共享资源时,如果没有使用监视器来进行同步,就会造成数据不一致的问题。例如,一个线程正在修改一个共享变量,而另一个线程也在读取该变量的值,这时可能会得到一个过期的值,从而导致程序出错。
因此,在Java多线程编程中,使用监视器是一种重要的同步机制,它能够确保多线程程序的正确性和可靠性。同时,使用好监视器也能够提升程序的并发性能。
下面是一个使用监视器实现线程同步的Java代码实例:
class Counter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized void decrement() {
count--;
}
public synchronized int getCount() {
return count;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50; i++) {
counter.decrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount()); // 输出0
}
}
在上面的代码中,Counter
类表示一个计数器,它有一个count
成员变量,以及三个同步方法increment
、decrement
和getCount
用于对计数器进行增加、减少和获取操作。这些方法都使用synchronized
关键字进行同步,确保了在多线程环境中对计数器的操作都是互斥的。
在Main
类的main
方法中,创建了两个线程t1
和t2
,它们分别对计数器进行增加和减少操作。在调用start
方法启动线程之后,调用t1.join()
和t2.join()
方法等待线程执行完毕,然后输出计数器的值。运行程序后,输出结果为0,说明使用监视器成功保证了计数器操作的正确性和一致性。
12.4.8 volatile字段
Java中的volatile关键字可以用于声明字段,它会确保该字段的可见性和原子性。
当一个字段被声明为volatile时,任何对该字段的读写操作都将在主内存中进行。在Java中,线程可以有自己的本地缓存,因此一个线程可能会从自己的缓存中读取一个变量的值,而不是从主内存中读取。如果多个线程同时操作同一个非volatile字段,就有可能发生线程间的数据不一致问题。
使用volatile字段可以确保多个线程对同一个变量进行访问时,该变量的值是正确的。此外,volatile字段还可以保证对该字段的赋值操作具有原子性。
需要注意的是,虽然使用volatile字段可以保证可见性和一定程度的原子性,但它并不适用于所有的并发场景。在高并发情况下,使用锁或其他更高级别的同步机制可能更合适。
下面是一个简单的示例代码,演示如何在Java中使用volatile关键字来确保多个线程访问共享变量时的可见性和顺序性:
public class Counter {
private volatile int count = 0; // 定义volatile字段
public void increment() {
count++; // 非原子操作,但volatile关键字确保可见性和顺序性
}
public int getCount() {
return count; // volatile关键字确保可见性和顺序性
}
}
在上面的代码中,我们定义了一个Counter类,其中count字段被声明为volatile,以确保多个线程访问时的可见性和顺序性。increment()方法仅是一个简单的计数器,它将count字段递增。注意,这不是一个原子操作,但由于count是volatile的,因此对它的修改对其他线程立即可见。
另外,getCount()方法也使用了volatile关键字,以确保调用线程能看到其他线程对count字段的最新修改。
需要注意的是,虽然volatile关键字确保了可见性和顺序性,但它并不保证原子性。如果要实现原子性操作,应该使用原子类或锁等同步机制。
12.4.9 final变量
Java中的final变量是一种常量,它在被赋值后不能被修改。final变量可以在声明时被赋值,也可以在构造函数中被赋值。
final变量有以下特点:
-
一旦被赋值,就不能再次修改。因此,final变量通常用于表示常量,例如π的值可以用final关键字声明,确保它不能被修改。
-
final变量可以是基本数据类型或引用类型。若声明的final变量是一个引用类型变量,则这个变量只是保证指向的地址不变,但仍可以修改对象内部的状态。
-
final变量是线程安全的。因为final变量是不可变的,所以不需要做同步处理,也就不会出现多线程并发访问时的数据竞争问题。
在Java中,线程里面使用final变量是安全的,因为final变量是不可变的,所以不需要做同步处理,也就不会出现多线程并发访问时的数据竞争问题。
同时,由于final变量只能被赋值一次,因此在多线程环境下使用final变量可以保证程序的可读性和可维护性,因为多线程并发访问同一个final变量时,不必担心变量的值会被其他线程修改。
下面是一个使用final变量的示例代码:
public class ThreadWithFinalExample extends Thread {
private final int MAX_COUNT; // final变量在声明时被赋值
public ThreadWithFinalExample(int max) {
MAX_COUNT = max;
}
@Override
public void run() {
for (int i = 0; i < MAX_COUNT; i++) {
System.out.println("Thread " + this.getName() + " running, count: " + i);
}
}
public static void main(String[] args) {
ThreadWithFinalExample thread1 = new ThreadWithFinalExample(5);
ThreadWithFinalExample thread2 = new ThreadWithFinalExample(10);
thread1.start();
thread2.start();
}
}
在上面的代码中,我们创建了两个线程,分别使用final变量MAX_COUNT来控制循环次数。由于final变量是不可变的,所以多个线程并发访问时不必担心变量的值会被其他线程修改,从而保证线程的安全性。
12.4.10 原子性
Java原子性是指在多线程环境下,一个操作要么执行完毕,要么没执行,不存在执行了一半被中断的情况。Java提供了多种原子性操作的方式,包括使用synchronized关键字,使用原子类以及使用锁等。
常用的Java原子性操作包括:
-
synchronized关键字:在Java中,使用synchronized关键字可以保证多个线程并发访问时的同步性和可见性。在使用synchronized关键字时,要注意避免死锁等问题。
-
AtomicXXX类:Java提供了多种AtomicXXX类,可以实现对基本数据类型的原子性操作,包括AtomicInteger、AtomicLong、AtomicBoolean、AtomicReference等等。这些类提供了一系列的原子性操作方法,如get()、set()、compareAndSet()、incrementAndGet()、decrementAndGet()等等。
-
Lock接口:Java中的Lock接口也可以用来实现原子性操作。Lock接口提供了锁的获取和释放方法,确保在同一时刻只有一个线程可以访问共享资源,从而保证原子性。
下面是一个使用AtomicInteger类实现原子性操作的示例代码:
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
public static void main(String[] args) {
AtomicExample example = new AtomicExample();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
example.increment();
}
}).start();
}
// 等待线程执行完成
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count value: " + example.getCount());
}
}
在上面的示例代码中,我们使用AtomicInteger类来实现原子性操作,其中increment()方法使用了incrementAndGet()原子性操作方法,该方法具备原子性,可以保证多个线程并发访问时的线程安全性。运行结果会输出总数值为1000,证明原子性操作的确能够确保线程安全。
12.4.11 死锁
Java死锁是指多个线程互相持有对方需要的资源,从而陷入了无法继续执行的状态。简单来说,就是两个或多个线程拿到了对方需要的锁,并且不会释放,导致所有的线程都无法继续执行下去。
例如,在以下代码中,线程1和线程2分别获取lock1和lock2,然后执行代码块。假如线程1在执行完代码块1后需要获取lock2,而此时线程2已经持有了lock2,那么线程1就会等待线程2释放lock2。同时,线程2也需要获取lock1,而此时线程1已经持有了lock1,那么线程2就会等待线程1释放lock1。这样,两个线程就互相等待对方释放锁,从而造成死锁。
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(() -> {
synchronized(lock1) {
System.out.println("Thread 1 acquired lock1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(lock2) {
System.out.println("Thread 1 acquired lock2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized(lock2) {
System.out.println("Thread 2 acquired lock2");
synchronized(lock1) {
System.out.println("Thread 2 acquired lock1");
}
}
});
t1.start();
t2.start();
为了避免死锁,我们需要遵循以下规则:
-
避免持有多个锁,并尽量减少同步代码块的嵌套层数。
-
在获取锁时,按照固定的顺序获取。例如,对于两个锁A和B,所有线程必须先获取A锁后才能获取B锁,确保获取锁的顺序是一致的。
-
使用超时机制,避免一直等待获取锁的过程。如果在一定时间内无法获取到锁,就放弃。
-
使用tryLock()方法,尝试获取锁并设定超时时间。如果获取锁失败,可以经过一段时间后再次尝试获取锁。
-
尽量使用同步代码块而不是同步方法,因为同步方法会将整个方法加锁,而同步代码块可以只对需要同步的代码块加锁,从而减少锁的竞争。
总之,避免死锁的最好方式是通过设计良好的程序,遵循良好的编程习惯,并进行必要的测试和调试。
12.4.12 线程局部变量
线程局部变量是指每个线程都有自己独立的变量副本,不同的线程之间互不干扰。Java提供了ThreadLocal类来实现线程局部变量。
ThreadLocal类有一个泛型参数,用来指定该变量的类型。通过ThreadLocal类的set()方法可以设置线程局部变量的值,通过get()方法可以获取线程局部变量的值。例如:
ThreadLocal<Integer> value = new ThreadLocal<>();
// 在主线程中设置线程局部变量的值
value.set(10);
// 在子线程中获取线程局部变量的值
Thread t = new Thread(() -> {
int v = value.get();
System.out.println("Thread value: " + v);
});
t.start();
在上面的代码中,主线程中设置了一个线程局部变量value的值为10,然后创建了一个子线程,在子线程中获取了value的值,并输出到控制台。由于每个线程都有自己独立的变量副本,因此在子线程中获取到的value值为null。如果在子线程中调用set()方法设置value的值,那么对主线程中的value值不会有任何影响。
需要注意的是,每个线程局部变量都是必须通过ThreadLocal类的实例来访问的,因此在使用线程局部变量时,需要先创建一个ThreadLocal实例,并通过该实例来访问线程局部变量。
线程局部变量的主要作用是保证线程安全,因为不同的线程之间互不干扰,避免了线程之间的数据竞争。例如,如果需要实现一个计数器,在多线程环境下,可以使用线程局部变量来实现,每个线程有自己独立的计数器变量,不同线程之间互不干扰,从而保证计数器的线程安全。
12.4.13 为什么废弃stop和suspend方法
Java废弃stop和suspend方法是因为这些方法容易导致线程不安全和死锁问题。
-
stop方法会强制停止线程,而不会释放资源和锁,这可能会导致程序的数据不一致性和死锁问题。
-
suspend方法会暂停线程,但不会释放锁,这可能会导致其他线程阻塞而无法执行,从而导致死锁问题。
为了避免这些问题,Java建议使用更安全、更可控的方式来使用线程,例如使用wait和notify方法来实现线程的挂起和恢复。另外,可以使用Java5之后提供的并发包来实现更加安全和高效的多线程编程。
12.5 线程安全的集合
线程安全的集合是一种可以在多个线程同时访问时保证数据一致性的集合。以下是几种常见的线程安全的集合:
-
ConcurrentHashMap:是一种线程安全的哈希表,可以在多个线程同时访问时保证数据的一致性。
-
CopyOnWriteArrayList:是一种线程安全的动态数组,可以在多个线程同时访问时保证数据的一致性。
-
ConcurrentLinkedQueue:是一种线程安全的队列,可以在多个线程同时访问时保证数据的一致性。
-
BlockingQueue:是一种阻塞队列,可以在多个线程同时访问时保证数据的一致性。
-
Semaphore:是一种线程同步工具,可以限制同时访问某个资源的线程数量。
-
CountDownLatch:是一种线程同步工具,可以等待多个线程执行完毕后再执行。
-
ConcurrentSkipListSet:是一种线程安全的有序集合,可以在多个线程同时访问时保证数据的一致性和有序性。
12.5.1 阻塞队列
Java中的阻塞队列是一种特殊的队列,它具有阻塞功能,可以用于线程间的协作。在线程池、生产者消费者模式等场景下都经常使用阻塞队列。
常用的阻塞队列有:
-
ArrayBlockingQueue:基于数组的有界阻塞队列;
-
LinkedBlockingQueue:基于链表的有界/无界阻塞队列;
-
PriorityBlockingQueue:基于堆的无界阻塞队列;
-
SynchronousQueue:没有容量的阻塞队列,每个插入操作必须等待一个相应的删除操作,反之亦然。
阻塞队列提供了put、take等方法,这些方法对于已满的队列或者空的队列,会将调用线程挂起,直到队列有空间或者元素可用。
例如,生产者向队列中添加元素时,若队列已满,则会阻塞直至队列中有空间可用;而消费者从队列中取出元素时,若队列为空,则会阻塞直至队列中有元素可用。
使用阻塞队列可以让生产者和消费者之间的数据处理更加平滑,避免了可能的CPU过度占用和资源浪费。
下面是Java线程阻塞队列的一个示例代码:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class BlockingQueueExample {
public static void main(String[] args) {
// 创建一个阻塞队列
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 创建一个生产者线程并启动
Thread producer = new Thread(new Producer(queue));
producer.start();
// 创建一个消费者线程并启动
Thread consumer = new Thread(new Consumer(queue));
consumer.start();
}
// 生产者线程
static class Producer implements Runnable {
private final BlockingQueue<String> queue;
public Producer(BlockingQueue<String> q) {
queue = q;
}
public void run() {
try {
// 生产10个消息
for (int i = 0; i < 10; i++) {
String msg = "Message " + i;
System.out.println("Producer: " + msg);
// 将消息放到队列中
queue.put(msg);
// 休眠1秒
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 消费者线程
static class Consumer implements Runnable {
private final BlockingQueue<String> queue;
public Consumer(BlockingQueue<String> q) {
queue = q;
}
public void run() {
try {
// 消费10个消息
for (int i = 0; i < 10; i++) {
// 从队列中取出消息
String msg = queue.take();
System.out.println("Consumer: " + msg);
// 休眠2秒
Thread.sleep(2000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
该程序模拟了一个生产者不断向阻塞队列中生产消息,而消费者则不断从队列中消费消息的过程,即一个典型的生产者-消费者模型。通过使用阻塞队列,可以让生产者在队列满时自动阻塞,并在队列为空时通知消费者进行消费,从而实现线程间的同步。
下面是Java阻塞队列中常用的方法列表:
方法名 | 描述 |
---|---|
put(E e) | 将元素添加到队列的末尾,如果队列已满,则会阻塞等待直到队列中有空间可以添加元素。 |
take() | 从队列的开头取出并删除一个元素,如果队列为空,则会阻塞等待直到队列中有元素可以取出。 |
offer(E e, long timeout, TimeUnit unit) | 将元素添加到队列的末尾,如果队列已满,则会阻塞等待一定的时间直到队列中有空间可以添加元素。 |
poll(long timeout, TimeUnit unit) | 从队列的开头取出并删除一个元素,如果队列为空,则会阻塞等待一定的时间直到队列中有元素可以取出。 |
peek() | 返回队列中的第一个元素,但不删除它,如果队列为空,则返回null。 |
size() | 返回队列中当前的元素个数。 |
remainingCapacity() | 返回队列中剩余的空间大小。 |
isEmpty() | 如果队列中没有元素,则返回true,否则返回false。 |
contains(Object o) | 如果队列中包含指定元素,则返回true,否则返回false。 |
remove(Object o) | 从队列中删除指定的元素,如果删除成功,则返回true,否则返回false。 |
iterator() | 返回队列的迭代器。 |
12.5.2 高效的映射、集和队列
在Java中,有几个常用的高效映射、集和队列的实现:
-
HashMap:HashMap是一个基于哈希表的映射实现,可以快速地将键值对存储并检索。它的插入、删除和查找操作都是常数时间复杂度,因此它是处理大量数据的首选映射实现。
-
TreeSet:TreeSet是一个基于红黑树的集合实现,在添加元素时会自动进行排序。它的插入、删除和查找操作都是O(log n)的时间复杂度,因此它是处理需要排序的大量数据的首选集合实现。
-
PriorityQueue:PriorityQueue是一个基于堆的队列实现,它可以自动维护队列中元素的优先级。插入元素的时间复杂度是O(log n),取出元素的时间复杂度是常数时间复杂度,因此它是处理需要按优先级排序的大量数据的首选队列实现。
除了以上几种实现,还有其他的高效映射、集和队列的实现,如ConcurrentHashMap、ConcurrentSkipListSet和ConcurrentLinkedQueue等,它们都可以在多线程环境下安全地使用。需要根据具体的业务需求和数据结构特点来选择合适的实现。
12.5.3 映射条目的原子更新
映射条目的原子更新是指在多线程并发访问映射时,保证对映射中某一条目的修改是原子性的,即不会被其他线程中途修改或中断。原子更新操作可以通过Java中的ConcurrentHashMap类的putIfAbsent()方法、remove()方法、replace()方法等实现。这些方法能够确保在多线程并发访问映射时,对映射中某一条目的修改是线程安全的。在使用原子更新操作时,需要注意合理使用锁、避免死锁和饥饿等并发问题。
下面是Java中ConcurrentHashMap类中putIfAbsent()方法的代码示例:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
Integer oldValue = map.putIfAbsent("key", 1);
if (oldValue == null) {
System.out.println("key不存在,已插入1");
} else {
System.out.println("key已存在,值为" + oldValue);
}
在多线程并发访问ConcurrentHashMap对象时,putIfAbsent()方法可以确保对于同一个key,只有一个线程能够成功地向映射中插入值。如果已存在该key,则返回原有的值;如果不存在该key,则插入指定的值并返回null。因此,可以通过判断返回值是否为null来判断是否插入成功。这样可以保证对映射中某一条目的修改是原子性的。
12.5.4 对并发散列映射的批操作
并发散列映射是一种数据结构,它允许多个线程同时访问映射表中的不同部分。由于并发访问可能导致数据竞争和不一致的状态,因此需要使用批操作来确保正确性。
批操作是指一组操作,它们一起执行,共享某些状态,并且要么全部成功,要么全部失败。在并发散列映射中,批操作可以用来执行以下任务:
1.添加:向映射表中添加一个键值对。
2.删除:从映射表中删除一个键值对。
3.更新:更新映射表中的一个键值对。
4.查询:查询映射表中的一个键值对。
批操作可以通过以下方式实现:
1.锁定整个映射表:这种方式可以确保操作的互斥性,但是可能会导致性能问题,因为只允许一个线程访问映射表。
2.使用锁分离:这种方式将映射表分成多个部分,每个部分由一个锁来控制。这种方式可以提高并发性能,但是可能会导致死锁问题。
3.无锁批操作:这种方式利用无锁算法来执行并发操作,以提高并发性能。但是这种方式比较复杂,需要仔细考虑算法的正确性和并发性能。
总之,批操作是确保并发散列映射正确性和性能的关键。不同的批操作实现方式适用于不同的场景,开发者需要根据实际情况来选择适合自己的方式。
Java提供了ConcurrentHashMap
类来实现并发散列映射。ConcurrentHashMap
提供了以下批操作:
1.批处理添加:使用ConcurrentHashMap.putAll()
方法向映射表中添加多个键值对。
2.批处理删除:使用ConcurrentHashMap.keySet()
方法获取键的集合,然后使用ConcurrentHashMap.remove()
方法循环删除键值对。
3.批处理更新:使用ConcurrentHashMap.keySet()
方法获取键的集合,然后使用ConcurrentHashMap.replace()
方法循环更新键值对。
4.批处理查询:使用ConcurrentHashMap.entrySet()
方法获取键值对的集合,然后使用Stream
流来查询符合条件的键值对。
示例代码如下:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapExample {
public static void main(String[] args) {
// 创建并发散列映射对象
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
// 批处理添加
Map<Integer, String> batchAdd = new HashMap<>();
batchAdd.put(1, "hello");
batchAdd.put(2, "world");
map.putAll(batchAdd);
// 批处理删除
for (Integer key : map.keySet()) {
if (key == 1) {
map.remove(key);
}
}
// 批处理更新
for (Integer key : map.keySet()) {
String value = map.get(key);
if (key == 2) {
map.replace(key, value, "Jerry");
}
}
// 批处理查询
map.entrySet().stream().filter(entry -> entry.getKey() == 2)
.forEach(entry -> System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue()));
}
}
在这个示例中,我们使用了ConcurrentHashMap
提供的批操作来对映射表进行添加、删除、更新和查询。注意,ConcurrentHashMap
的批操作都是线程安全的,并且可以支持并发操作。
12.5.5 并发集视图
Java 并发集是指多个线程可以同时访问和修改的集合。Java 提供了多种并发集实现,包括:
-
ConcurrentHashMap:线程安全的哈希表,支持高并发的读写操作。
-
ConcurrentLinkedQueue 和 ConcurrentLinkedDeque:线程安全的队列和双端队列,支持高并发的入队和出队操作。
-
CopyOnWriteArrayList 和 CopyOnWriteArraySet:线程安全的可变数组和集合,支持高并发的读操作和低并发的写操作。
-
BlockingQueue 和 BlockingDeque:支持阻塞操作的队列和双端队列,用于控制线程的执行顺序。
-
LinkedBlockingQueue 和 LinkedBlockingDeque:基于链表的阻塞队列和双端队列。
-
PriorityBlockingQueue:基于优先级的阻塞队列。
以上并发集都是线程安全的,可以在多线程环境下使用。在使用时应该根据需求选择适合的实现,以达到最优的性能和效果。
以下是 Java 并发集视图的一些示例代码:
- ConcurrentHashMap:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
// 遍历并发哈希表
map.forEach((k, v) -> System.out.println(k + ":" + v));
// 取出指定键的值
int value = map.get("A");
System.out.println("Value of A is " + value);
// 尝试替换指定键的值
boolean replaced = map.replace("B", 2, 4);
if (replaced) {
System.out.println("Value of B replaced with 4 successfully!");
} else {
System.out.println("Failed to replace value of B!");
}
- ConcurrentLinkedQueue:
ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>();
queue.offer("A");
queue.offer("B");
queue.offer("C");
// 顺序弹出队列中的元素
String element = queue.poll();
while (element != null) {
System.out.println("Element: " + element);
element = queue.poll();
}
// 获取队列的大小
int size = queue.size();
System.out.println("Queue size is " + size);
- CopyOnWriteArrayList:
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("A");
list.add("B");
list.add("C");
// 遍历并发可变数组
for (String element : list) {
System.out.println("Element: " + element);
}
// 尝试替换指定索引的元素
boolean replaced = list.set(1, "D");
if (replaced) {
System.out.println("Element at index 1 replaced with D successfully!");
} else {
System.out.println("Failed to replace element at index 1!");
}
12.5.6 写数组的拷贝
在Java中,数组的拷贝可以通过System.arraycopy()方法或者Arrays.copyOf()方法来实现。
下面是使用System.arraycopy()方法实现数组拷贝的示例代码:
int[] arr1 = {1, 2, 3, 4, 5};
int[] arr2 = new int[arr1.length];
System.arraycopy(arr1, 0, arr2, 0, arr1.length);
System.out.println(Arrays.toString(arr2)); // 输出:[1, 2, 3, 4, 5]
在上面的代码中,首先定义了一个整型数组arr1和一个长度与arr1相同的空数组arr2。然后使用System.arraycopy()方法将arr1数组的所有元素复制到arr2数组中。该方法的参数依次为:源数组、源数组起始索引、目标数组、目标数组起始索引、要复制的元素个数。
下面是使用Arrays.copyOf()方法实现数组拷贝的示例代码:
int[] arr1 = {1, 2, 3, 4, 5};
int[] arr2 = Arrays.copyOf(arr1, arr1.length);
System.out.println(Arrays.toString(arr2)); // 输出:[1, 2, 3, 4, 5]
在上面的代码中,首先定义了一个整型数组arr1,然后使用Arrays.copyOf()方法将arr1数组的所有元素复制到一个新数组arr2中。该方法的参数依次为:源数组、目标数组长度。
12.5.7 并行数组算法
在Java中,并行数组算法主要是通过Java并发包中的Fork/Join框架来实现的。Fork/Join框架提供了一种简单易用的方式来将一个大任务拆分成多个小任务进行并行计算,然后将小任务的结果合并成大任务的最终结果。
下面是一个简单的示例代码,使用Fork/Join框架实现对数组进行求和:
import java.util.concurrent.RecursiveTask;
public class ArraySumTask extends RecursiveTask<Long> {
private long[] array;
private int start;
private int end;
private static final int THRESHOLD = 10000;
public ArraySumTask(long[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
if (end - start <= THRESHOLD) {
long sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
} else {
int mid = (start + end) / 2;
ArraySumTask left = new ArraySumTask(array, start, mid);
ArraySumTask right = new ArraySumTask(array, mid, end);
left.fork();
long rightResult = right.compute();
long leftResult = left.join();
return leftResult + rightResult;
}
}
public static long arraySum(long[] array) {
return new ArraySumTask(array, 0, array.length).compute();
}
}
在上面的代码中,ArraySumTask类继承自RecursiveTask,表示可以将任务拆分成多个子任务进行并行计算。其中,compute()方法是实际的任务执行方法,如果拆分的任务范围不超过阈值(这里设为THRESHOLD),则直接对任务范围内的数组元素进行求和;否则,将任务拆分成两个子任务,分别计算左半部分和右半部分的和,并将左半部分的任务异步执行(即调用fork()方法),右半部分的任务同步执行(即直接调用compute()方法),最后将两个子任务的结果合并。
在计算数组的和时,可以通过调用ArraySumTask.arraySum()方法来启动计算,并且该方法会自动创建一个ForkJoinPool来管理任务的执行。例如:
long[] array = new long[1000000];
for (int i = 0; i < array.length; i++) {
array[i] = i + 1;
}
long sum = ArraySumTask.arraySum(array);
System.out.println(sum); // 输出:500000500000
在上面的代码中,首先定义了一个包含1000000个元素的长整型数组,然后通过调用ArraySumTask.arraySum()方法来计算数组的和,最后将结果输出。
12.5.8 较早的线程安全集合
在Java较早的版本中,并没有像现在这样完善的线程安全集合框架,但是Java提供了一些基本的线程安全集合,如下:
-
Vector类:Vector是Java集合框架中的线程安全类,它的实现方式是通过使用synchronized来保证线程安全。Vector在插入、删除和查找元素时都是线程安全的,因此可以在多线程环境中使用。
-
Hashtable类:Hashtable是Java集合框架中的线程安全类,它的实现方式也是通过使用synchronized来保证线程安全。Hashtable支持键值对的插入、删除和查找操作,且在多线程环境下也可以保证线程安全。
-
Stack类:Stack是Java集合框架中的线程安全类,它继承自Vector类,因此具有Vector类的线程安全性。Stack是一种先进后出(FILO)的数据结构,支持压入元素、弹出元素和查找元素的操作。
虽然这些线程安全集合在Java集合框架中已经不再推荐使用,但是在较早版本的Java中,在多线程环境下使用这些集合类是一种较为简单的方式来保证线程安全。但是需要注意的是,这些集合类的性能可能不如现在的并发集合,因此在性能要求较高的场景下,建议使用并发集合来替代这些线程安全集合。
以下是一些Java较早的线程安全集合的示例代码:
- Vector类的使用
import java.util.Vector;
public class VectorExample {
public static void main(String[] args) {
Vector<String> vector = new Vector<String>();
vector.add("Java");
vector.add("Python");
vector.add("C++");
System.out.println("Vector elements:");
for (String str : vector) {
System.out.println(str);
}
}
}
- Hashtable类的使用
import java.util.Hashtable;
public class HashtableExample {
public static void main(String[] args) {
Hashtable<Integer, String> hashtable = new Hashtable<Integer, String>();
hashtable.put(1, "Java");
hashtable.put(2, "Python");
hashtable.put(3, "C++");
System.out.println("Hashtable elements:");
for (Integer key : hashtable.keySet()) {
System.out.println(key + " : " + hashtable.get(key));
}
}
}
- Stack类的使用
import java.util.Stack;
public class StackExample {
public static void main(String[] args) {
Stack<Integer> stack = new Stack<Integer>();
stack.push(1);
stack.push(2);
stack.push(3);
System.out.println("Initial Stack: " + stack);
System.out.println("Popping the top element: " + stack.pop());
System.out.println("Stack after popping: " + stack);
System.out.println("Pushing a new element: " + stack.push(4));
System.out.println("Stack after pushing: " + stack);
}
}
这些示例代码展示了如何使用Java较早的线程安全集合类来进行基本的操作,如插入、删除和查找元素。虽然这些集合类在现在的Java集合框架中已经不推荐使用,但是它们仍然在一些旧系统或遗留代码中被使用,因此了解它们的基本用法仍然有一定的意义。
12.6 任务和线程池
Java中任务(Task)和线程池(ThreadPool)是常见的多线程编程技术。任务是指一个可执行的代码块,线程池则为管理和执行多个任务提供了一个机制。下面分别介绍Java中任务和线程池的基本概念和使用方法。
任务
在Java中,任务可以通过实现Runnable接口或Callable接口来创建。Runnable接口表示一个可执行的代码块,没有返回值;Callable接口也表示一个可执行的代码块,但它有返回值,并且可能抛出异常。下面分别给出这两种接口的示例代码:
// 实现Runnable接口
class MyRunnable implements Runnable {
@Override
public void run() {
// 执行任务代码
}
}
// 实现Callable接口
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
// 执行任务代码
return 1; // 返回一个数值
}
}
可以把Runnable和Callable对象提交给ExecutorService线程池来执行,示例代码如下:
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(new MyRunnable());
Future<Integer> future = executor.submit(new MyCallable());
其中,executor.submit()方法用于提交一个任务,返回一个Future对象,可以用于获取任务的执行结果。注意,在这个例子中创建了一个固定大小为2的线程池,即线程池中最多同时执行两个任务。
线程池
Java中线程池是通过Executor框架来实现的,主要有三种类型的线程池:FixedThreadPool、CachedThreadPool和ScheduledThreadPool。这些线程池都实现了ExecutorService接口。
FixedThreadPool
FixedThreadPool是一个固定大小的线程池,其中的线程数是固定的。如果有多个任务需要执行,这些任务会被放到一个任务队列中,线程池中的每个线程会从队列中获取一个任务并执行,一直到队列为空。由于线程数是固定的,因此它可以避免创建过多的线程而导致资源消耗过大。示例代码如下:
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 10; i++) {
executor.execute(new MyRunnable());
}
executor.shutdown();
在这个例子中,创建了一个大小为2的FixedThreadPool,并提交了10个任务。注意,在提交完任务后需要调用executor.shutdown()方法来关闭线程池。
CachedThreadPool
CachedThreadPool是一个可缩放的线程池,其中的线程数会根据任务的数量来动态调整。如果有新的任务需要执行,线程池会创建新的线程来处理这些任务,而如果某个线程在执行任务时空闲了一段时间,它就会被销毁。由于线程数是动态的,因此它可以避免创建过多的线程而导致资源消耗过大。示例代码如下:
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executor.execute(new MyRunnable());
}
executor.shutdown();
在这个例子中,创建了一个大小为0的CachedThreadPool,并提交了10个任务。注意,在提交完任务后需要调用executor.shutdown()方法来关闭线程池。
ScheduledThreadPool
ScheduledThreadPool是一个定时执行任务的线程池,其中的任务可以按照一定的时间间隔来执行。它可以用于定时刷新缓存、定时发送邮件等定时任务。示例代码如下:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(new MyRunnable(), 0, 1, TimeUnit.SECONDS);
在这个例子中,创建了一个大小为1的ScheduledThreadPool,每隔1秒就会执行一个任务。注意,在提交完任务后需要调用executor.shutdown()方法来关闭线程池。
这些是Java中任务和线程池的基本概念和用法。在实际应用中,任务和线程池可以结合使用,以提高多线程编程的效率和可靠性。
12.6.2 Callable 与 Future
Callable 是 Java 接口之一,它代表一个可以被执行的任务,并且能够返回结果、抛出异常。Callable 接口只包含一个方法 call(),当任务执行完毕后,它会返回一个类型为泛型的值,或者抛出一个可以由异常类型变量指定的异常。
Future 表示一个异步计算结果的接口,也是 Java 的接口之一。它提供了一些方法,可以查询异步任务是否完成、等待执行结果、取消异步任务等。Future 接口还扩展了 Java 的另一个接口 Runnable,因此,它可以被提交到 Executor 进行执行。Future 接口的方法 get() 可以获取异步任务的结果。
Callable 接口和 Future 接口是紧密相关的,Callable 接口可以表示一个异步计算任务,而 Future 接口可以表示这个计算任务的结果。当我们提交一个 Callable 对象到 Executor,它就会返回一个 Future 对象,我们可以通过这个对象来获取异步计算任务的结果。因此,可以说 Callable 和 Future 是 Java 并发编程中非常重要的两个接口。
下面是一个简单的 Callable 和 Future 的例子,它会创建一个 Callable 对象并使用 ExecutorService 提交任务,然后通过 Future 对象获取异步计算的结果:
import java.util.concurrent.*;
public class CallableFutureExample {
public static void main(String[] args) throws Exception {
// 创建一个 Callable 对象
Callable<Integer> task = () -> {
int sum = 0;
for (int i = 1; i <= 1000; i++) {
sum += i;
}
// 返回计算结果
return sum;
};
// 创建 ExecutorService
ExecutorService executor = Executors.newSingleThreadExecutor();
// 提交任务并获取 Future 对象
Future<Integer> future = executor.submit(task);
// 获取异步计算的结果
int result = future.get();
System.out.println("计算结果:" + result);
// 关闭 ExecutorService
executor.shutdown();
}
}
在上面的例子中,我们首先创建了一个 Callable 对象,它会计算 1 到 1000 的和并返回结果。然后使用 ExecutorService 提交任务并获取 Future 对象,最后通过 Future 对象获取异步计算的结果并打印输出。
需要注意的是,在调用 future.get() 方法时,它会阻塞当前线程直到异步计算任务完成并获取结果。如果异步任务在获取结果之前抛出异常,future.get() 方法也会抛出相应的异常。因此,需要在适当的时候捕获异常并处理。
12.6.2 执行器
Java中的执行器是一种通过线程池来管理和调度线程的机制,它可以异步执行任务,并且可以在执行任务时控制线程的数量,从而更好地利用系统资源。Java中的执行器主要有以下几种:
-
Executors:这是一个工厂类,用于创建各种类型的执行器,例如单线程执行器、固定线程池执行器、可缓存线程池执行器等。
-
ExecutorService:这是一个接口,它继承自Executor接口,定义了执行器的基本操作,例如提交任务、关闭执行器等。
-
ThreadPoolExecutor:这是一个实现ExecutorService接口的类,它通过一个线程池来执行任务。线程池中的线程数量可以根据需要进行动态调整。
-
ScheduledExecutorService:这是一个接口,它继承自ExecutorService接口,定义了可以按照一定的时间计划执行任务的执行器。
下面是一个使用Java执行器的示例代码:
import java.util.concurrent.*;
public class ExecutorDemo {
public static void main(String[] args) {
// 创建一个线程池执行器
ExecutorService executor = Executors.newFixedThreadPool(2);
// 提交任务
Future<String> future1 = executor.submit(new Task("task1"));
Future<String> future2 = executor.submit(new Task("task2"));
Future<String> future3 = executor.submit(new Task("task3"));
// 关闭执行器
executor.shutdown();
// 输出任务结果
try {
System.out.println("task1 result: " + future1.get());
System.out.println("task2 result: " + future2.get());
System.out.println("task3 result: " + future3.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
static class Task implements Callable<String> {
private String name;
public Task(String name) {
this.name = name;
}
@Override
public String call() throws Exception {
System.out.println(name + " is running on " + Thread.currentThread().getName());
Thread.sleep(2000);
return name + " is done";
}
}
}
输出结果为:
task1 is running on pool-1-thread-1
task2 is running on pool-1-thread-2
task1 result: task1 is done
task3 is running on pool-1-thread-1
task2 result: task2 is done
task3 result: task3 is done
从输出结果可以看出,任务是异步执行的,它们在不同的线程中运行。执行器管理了两个线程,因此一次只有两个任务可以同时执行。在所有任务执行完成之后,执行器被关闭,这意味着不再接受新的任务,同时等待正在进行的任务执行完成。最后,我们通过Future对象获取了任务的执行结果。
12.6.3 控制任务组
Java中可以使用执行器来控制任务的执行,同时也可以对一组任务进行控制,例如等待所有任务完成或者只需要等待其中任意一个任务完成等等。Java提供了一些控制任务组的工具类和接口,如CountDownLatch、CyclicBarrier、Semaphore等。其中,CountDownLatch是一个非常常见的工具,可以用于等待一组任务的完成。
CountDownLatch的工作方式类似于一个门闩,我们可以把它看作是一个计数器,通过计数器的值来控制任务的执行。当计数器的值为0时,所有等待该计数器的线程可以继续执行。我们可以使用CountDownLatch来等待一组任务都完成后再进行下一步操作。
下面是一个使用CountDownLatch来控制任务组的示例代码:
import java.util.concurrent.*;
public class TaskGroupDemo {
public static void main(String[] args) throws InterruptedException {
int taskCount = 5;
// 创建计数器
CountDownLatch latch = new CountDownLatch(taskCount);
// 创建线程池执行器
ExecutorService executor = Executors.newFixedThreadPool(2);
// 提交任务
for (int i = 0; i < taskCount; i++) {
executor.submit(new Task("task" + i, latch));
}
// 等待所有任务完成
latch.await();
// 关闭执行器
executor.shutdown();
System.out.println("all tasks are done");
}
static class Task implements Runnable {
private String name;
private CountDownLatch latch;
public Task(String name, CountDownLatch latch) {
this.name = name;
this.latch = latch;
}
@Override
public void run() {
System.out.println(name + " is running on " + Thread.currentThread().getName());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
System.out.println(name + " is done");
}
}
}
在这个示例中,我们创建了一个计数器CountDownLatch,它的值为任务数量。在每个任务完成时,我们将计数器的值减1,最终当所有任务完成后,计数器的值为0,等待该计数器的主线程可以继续执行。在这个例子中,我们等待所有任务完成后输出"all tasks are done"。
12.6.4 fork - join 框架
Java中的Fork-Join框架是一种用于并行计算的框架,它可以将一个大的计算任务拆分成若干个小任务,然后分别执行这些小任务并将结果合并返回。
Fork-Join框架的核心是Fork-Join线程池,它使用了一种特殊的工作窃取算法,可以在保证线程安全的前提下,让所有的工作线程忙碌起来,充分利用计算资源。
Fork-Join框架的使用分为三个步骤:
- 创建ForkJoinPool
ForkJoinPool pool = new ForkJoinPool();
- 创建ForkJoinTask
在ForkJoin框架中,我们可以使用两种类型的任务:ForkJoinTask和RecursiveTask。其中ForkJoinTask是一类无返回值的任务,而RecursiveTask是一类有返回值的任务,它们分别对应了两个方法:fork()和compute()。在这里我们使用RecursiveTask来进行示例。
class MyTask extends RecursiveTask<Integer> {
@Override
protected Integer compute() {
// 计算任务的结果
return result;
}
}
- 提交任务并获得结果
MyTask task = new MyTask();
Integer result = pool.invoke(task);
完整的示例代码如下:
import java.util.concurrent.*;
public class ForkJoinDemo {
public static void main(String[] args) {
ForkJoinPool pool = new ForkJoinPool();
MyTask task = new MyTask(0, 10);
Integer result = pool.invoke(task);
System.out.println(result);
}
static class MyTask extends RecursiveTask<Integer> {
private int start;
private int end;
public MyTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if (end - start <= 2) {
int sum = 0;
for (int i = start; i <= end; i++) {
sum += i;
}
return sum;
} else {
int mid = (start + end) / 2;
MyTask leftTask = new MyTask(start, mid);
MyTask rightTask = new MyTask(mid + 1, end);
leftTask.fork();
rightTask.fork();
int leftResult = leftTask.join();
int rightResult = rightTask.join();
return leftResult + rightResult;
}
}
}
}
在这个示例中,我们使用了Fork-Join框架来计算0到10的累加和。我们创建了一个MyTask任务,它会将计算范围不断拆分,直到范围足够小,然后进行计算并返回结果。在拆分时,我们使用了fork()方法来提交任务,使用join()方法来获得子任务的结果。
需要注意的是,在Fork-Join框架中,任务的拆分是递归进行的,因此我们需要设置递归终止条件。同时,在拆分任务时也需要注意任务的大小,过小的任务会造成线程调度的开销,过大的任务也会影响到并行计算的效率。
12.7 异步计算
Java异步计算指的是在主线程外,以非阻塞方式执行耗时的任务,并在任务完成后通知主线程继续执行,以提高程序性能和响应能力。
在Java中,异步计算可以通过多线程、回调和Future等方式实现。其中,多线程是最常见的实现方式,可以通过创建新的线程或者使用线程池来执行异步任务,任务执行完成后通过主线程等待和获取结果。
异步计算可以应用于各种需要执行耗时任务的场景,如网络请求、IO操作、复杂计算等。通过异步计算,程序可以在任务执行期间继续响应用户的输入,以提高用户体验和程序性能。
以下是一个简单的使用Java多线程实现异步计算的示例代码:
import java.util.concurrent.*;
public class AsyncExample {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
// 模拟一个耗时的任务
Thread.sleep(2000);
return "Hello, world!";
});
// 在主线程中等待任务完成并获取结果
while (!future.isDone()) {
System.out.println("Task is not yet complete...");
Thread.sleep(1000);
}
System.out.println(future.get()); // 输出结果
executor.shutdown(); // 关闭线程池
}
}
在上面的示例中,我们使用Executors
类创建了一个单线程的线程池,并通过executor.submit()
方法提交一个任务。任务是一个使用Lambda表达式编写的匿名函数,其中包含一个模拟耗时2秒的任务。
通过Future
对象我们可以在主线程中等待任务完成并获取结果。future.isDone()
方法可以判断异步任务是否已经完成,如果未完成则继续等待,否则调用future.get()
方法获取结果。
最后我们需要关闭线程池以释放资源。
12.7.1 可完成Future
Future(未来)可以通过使用异步编程语言或编程库/框架来完成。一些流行的异步编程语言包括JavaScript和Python中的async/await。在Java中,可以使用Java 8中的CompletableFuture类来完成Future。
以下是一个使用Java 8 CompletableFuture的示例:
import java.util.concurrent.CompletableFuture;
public class FutureExample {
public static void main(String[] args) throws Exception {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// 执行异步操作
return "Hello, World!";
});
future.thenAccept(result -> {
// 异步操作完成后的回调函数
System.out.println(result);
});
System.out.println("Waiting for future to complete...");
Thread.sleep(1000);
System.out.println("Done.");
}
}
在上面的示例中,使用CompletableFuture.supplyAsync
方法创建了一个异步操作的执行线程,并返回一个CompletableFuture
对象。然后,thenAccept
方法定义了一个回调函数,该函数在异步操作完成后被调用,并打印出异步操作的结果。最后,程序等待异步操作完成,然后输出“Done.”。
这只是异步编程的基础,异步编程还包括许多其他的高级概念和技术,如Promise、RxJava等,但是使用CompletableFuture可以实现Future的基本功能。
12.7.2 组合可完成Future
Java中的CompletableFuture类提供了组合多个CompletableFuture的方法,这些方法允许我们将多个异步任务组合在一起,并在它们都完成时进行操作或处理它们的结果。
组合CompletableFuture的方法可以分为两种类型:
- 等待所有的CompletableFuture完成。
allOf()
方法可以等待所有的CompletableFuture执行完成,并将它们的结果组合在一起处理。 - 等待任何一个CompletableFuture完成。
anyOf()
方法可以等待任何一个CompletableFuture完成,并处理第一个完成的结果。
这些方法都返回一个新的CompletableFuture,该CompletableFuture将在所有或任何一个子任务完成时完成。使用这些方法可以很容易地创建一些复杂的异步计算流程。
以下是一个使用Java CompletableFuture类组合可完成Future的代码实例:
public static void main(String[] args) throws ExecutionException, InterruptedException {
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> {
int result = 1;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Future1 finished");
return result;
});
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> {
int result = 2;
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Future2 finished");
return result;
});
CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> {
int result = 3;
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Future3 finished");
return result;
});
CompletableFuture<Integer> finalFuture = CompletableFuture.allOf(future1, future2, future3)
.thenApply(v -> Stream.of(future1, future2, future3).mapToInt(CompletableFuture::join).sum());
System.out.println("Final result: " + finalFuture.get());
}
在这个例子中,我们创建了三个CompletableFuture,每个CompletableFuture模拟了一个需要1秒、2秒和3秒才能完成的任务。我们使用allOf
方法等待所有的CompletableFuture执行完成。然后,我们使用thenApply
方法对所有的结果进行处理,将它们加起来得到最终结果。
输出结果:
Future1 finished
Future2 finished
Future3 finished
Final result: 6
注意:在使用CompletableFuture进行异步计算时,一定要小心处理异常,否则可能会导致整个应用程序崩溃。建议在使用exceptionally
方法对异常进行处理。
下表列出了Java中CompletableFuture类中可用的一些增加动作的方法:
方法名 | 返回类型 | 描述 |
---|---|---|
thenApply() | CompletableFuture | 将一个函数应用于当前CompletableFuture的结果,在新CompletableFuture中返回一个结果 |
thenAccept() | CompletableFuture | 在当前CompletableFuture的结果上执行一个Action动作,返回一个空结果 |
thenRun() | CompletableFuture | 在当前CompletableFuture的结果上执行一个Runnable动作,返回一个空结果 |
thenCombine() | CompletableFuture | 将两个CompletableFuture的结果合并成一个结果,返回一个新的ComletableFuture |
thenCompose() | CompletableFuture | 将当前CompletableFuture的结果传递给一个函数,该函数将创建并返回一个新的CompletableFuture,该CompletableFuture将被执行 |
这些方法将异步任务进行组合,并允许您在结果可用时对它们采取行动。
组合多个对象(Future)的方法
方法名 | 描述 |
---|---|
Future.all(iterable) | 当 iterable 中所有的 Future 都完成时,返回一个 Future,它的结果为一个 List,包含每个 Future 的结果。若其中一个 Future 失败,则返回的 Future 状态为失败。 |
Future.any(iterable) | 当 iterable 中任意一个 Future 完成时,返回一个 Future,它的结果为第一个完成的 Future 的结果。若 iterable 中没有 Future,则返回的 Future 状态为失败。 |
Future.wait(futures, {bool eagerError}) | 等待 futures 中所有 Future 完成。若有其中一个 Future 失败,则返回的 Future 状态为失败。可以设置参数 eagerError 为 true,在第一个 Future 失败时就立即返回。返回的 Future 的结果为一个 List,包含每个 Future 的结果。 |
Future.doWhile(bool test()) | 执行 test() 函数并等待其返回,若结果为 true,则执行 body() 函数并继续执行 test() 函数,直到 test() 返回 false。返回的 Future 的结果为 null。 |
Future.forEach(Iterable input, Future f(T element)) | 对 input 中的每个元素,执行 f(element) 函数并等待其完成。返回的 Future 的结果为 null。 |
Future.reduce(Iterable input, Future combine(T previous, T element)) | 对 input 中的所有元素,依次执行 combine() 函数并等待其完成。combine() 函数接收前一个元素和当前元素作为参数,返回一个 Future。返回的 Future 的结果为最后一次 combine() 调用的结果。 |
12.7.3 用户界面回调中的长时间运行任务
在 Java 用户界面中,长时间运行的任务可以导致界面卡顿或无响应的情况,因此需要使用回调来解决这个问题。回调机制可以在后台线程中执行长时间运行的任务,而不会影响用户界面的响应。以下是实现回调机制的步骤:
- 创建一个接口,其中包含长时间运行任务的方法和任务完成后的回调方法。
public interface LongRunningTaskCallback {
public void onTaskStart();
public void onTaskComplete();
public void onTaskError(Exception e);
public void onTaskProgress(int progress);
}
- 在用户界面中调用长时间运行任务的方法,并传入回调接口的实现类作为参数。
public void startLongRunningTask(LongRunningTaskCallback callback) {
callback.onTaskStart();
// 执行长时间运行的任务,例如下载文件
try {
int progress = 0;
while (progress < 100) {
// ... 下载文件的逻辑 ...
Thread.sleep(1000); // 模拟耗时操作
progress += 10;
callback.onTaskProgress(progress);
}
callback.onTaskComplete();
} catch (Exception e) {
callback.onTaskError(e);
}
}
- 在回调接口的实现类中实现回调方法,更新用户界面的状态,例如显示进度条、提示用户任务完成等。
public class MyLongRunningTaskCallback implements LongRunningTaskCallback {
private JProgressBar progressBar;
public MyLongRunningTaskCallback(JProgressBar progressBar) {
this.progressBar = progressBar;
}
@Override
public void onTaskStart() {
progressBar.setValue(0);
progressBar.setString("任务开始");
}
@Override
public void onTaskComplete() {
progressBar.setValue(100);
progressBar.setString("任务完成");
}
@Override
public void onTaskError(Exception e) {
progressBar.setValue(0);
progressBar.setString("任务出错");
JOptionPane.showMessageDialog(null, e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
}
@Override
public void onTaskProgress(int progress) {
progressBar.setValue(progress);
progressBar.setString("下载进度:" + progress + "%");
}
}
- 在用户界面中创建回调接口的实现类对象,并将其作为参数传递给长时间运行任务的方法。
JProgressBar progressBar = new JProgressBar();
MyLongRunningTaskCallback callback = new MyLongRunningTaskCallback(progressBar);
startLongRunningTask(callback);
通过以上步骤,可以在用户界面中执行长时间运行的任务,同时不会导致界面卡顿或无响应。
12.8 进程
Java 进程是指 Java 虚拟机(JVM)在操作系统中的一个运行实例。在 Java 中,每个应用程序或线程都是在 JVM 中独立运行的进程。
一个 Java 进程在操作系统中被视为一个单独的进程,它可以被启动、停止和管理,就像任何其他进程一样。Java 进程具有自己的内存空间、线程和执行环境,可以运行任何 Java 应用程序。
Java 进程通常由 Java 应用程序、Web 应用程序或服务器启动,它们使用 Java 命令来启动一个 Java 虚拟机实例。例如,以下命令启动一个使用默认设置的 Java 虚拟机:
java MyApplication
Java 进程可以通过 Java 管理扩展(JMX)API 进行远程管理和监视。JMX API 提供了一种标准化的方法来访问 Java 进程的内部状态和管理功能,可以用来监视系统性能、诊断问题、优化代码等。
Java 进程还可以与其他进程进行通信,例如使用 Java 远程方法调用(RMI)协议来访问远程对象,或使用 Java 消息服务(JMS)协议来发送和接收异步消息。
总之,Java 进程是 Java 应用程序在操作系统中的运行实例,它具有独立的内存空间、线程和执行环境,并可以通过 JMX API 进行远程管理和监视。
12.8.1 建立一个进程
Java中可以使用ProcessBuilder类来创建一个新进程。下面是一个简单的例子:
import java.io.IOException;
public class ProcessBuilderDemo {
public static void main(String[] args) {
try {
// 创建一个新的进程
ProcessBuilder pb = new ProcessBuilder("notepad.exe");
// 启动进程
Process p = pb.start();
// 等待进程执行结束
p.waitFor();
System.out.println("进程已经结束!");
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
此示例创建一个新的进程并启动记事本应用程序(notepad.exe)。使用start()方法启动进程,并使用waitFor()方法等待进程执行结束。
12.8.2 运行一个进程
在 Java 中,可以使用 ProcessBuilder
或者 Runtime
类来启动一个进程。
使用 ProcessBuilder
类启动进程:
ProcessBuilder processBuilder = new ProcessBuilder("command", "arg1", "arg2");
Process process = processBuilder.start();
其中,command
参数表示要执行的命令,arg1
和 arg2
表示命令的参数。
使用 Runtime
类启动进程:
Runtime runtime = Runtime.getRuntime();
Process process = runtime.exec("command arg1 arg2");
其中,command arg1 arg2
表示要执行的命令和参数。
在运行进程的同时,你还可以使用 InputStream
和 OutputStream
来与进程进行交互,并获取进程的输出结果。例如:
Process process = runtime.exec("command arg1 arg2");
InputStream inputStream = process.getInputStream();
OutputStream outputStream = process.getOutputStream();
更多关于 Java 进程的使用方法,请参考 Java 文档。
12.8.3 进程句柄
Java 进程句柄(又称进程标识符)是一个唯一的标识符,用于标识正在运行的 Java 进程。它是由操作系统分配和管理的。在 Java 中,可以通过调用 Process
类的 pid()
方法来获取进程句柄。例如:
Process process = Runtime.getRuntime().exec("notepad.exe");
long pid = process.pid();
System.out.println("进程句柄:" + pid);
该代码会启动 Windows 记事本应用程序,并输出其进程句柄。在不同的操作系统上,获取进程句柄的方式可能会有所不同。例如,在 Linux 上,可以使用 ps
命令来查看所有正在运行的进程及其标识符。
附录 Java关键字
关键字 | 描述 |
---|---|
abstract | 抽象方法的修饰符 |
assert | 断言条件是否为真 |
boolean | 布尔类型数据 |
break | 终止循环 |
byte | 字节类型数据 |
case | switch 语句中的分支 |
catch | 捕获异常的处理块 |
char | 字符类型数据 |
class | 定义类 |
const | 常量声明 |
continue | 继续下一次循环执行 |
default | switch 语句中的默认分支 |
do | 循环语句的起始关键字 |
double | 双精度浮点类型数据 |
else | if 语句的分支 |
enum | 枚举类型数据 |
extends | 继承类的关键字 |
final | 声明最终的变量 |
finally | try-catch 语句中的最后执行块 |
float | 单精度浮点类型数据 |
for | 循环语句的起始关键字 |
goto | 标记语句 |
if | 条件语句的起始关键字 |
implements | 实现接口 |
import | 导入包 |
instanceof | 判断对象是否为某个类的实例 |
int | 整数类型数据 |
interface | 定义接口 |
long | 长整型数据 |
native | 声明使用本地方法 |
new | 创建新的对象 |
package | 定义包 |
private | 私有数据修饰符 |
protected | 受保护数据修饰符 |
public | 公共数据修饰符 |
return | 返回值 |
short | 短整型数据 |
static | 静态变量、方法和语句块修饰符 |
strictfp | 精确浮点运算 |
super | 调用超类的方法 |
switch | 多分支条件语句 |
synchronized | 同步方法和语句块 |
this | 对象本身的引用 |
throw | 手动抛出异常 |
throws | 方法声明可能抛出的异常 |
transient | 不参与序列化的变量修饰符 |
try | 定义异常处理块 |
void | 无返回值的方法 |
volatile | 线程安全数据修饰符 |
while | 循环语句的起始关键字 |
以下是Java关键字的实例代码:
// abstract
public abstract class Shape {
public abstract double area();
}
// assert
int i = 10;
assert i == 10;
// boolean
boolean flag = true;
// break
for (int i = 0; i < 10; i++) {
if (i == 5) {
break;
}
}
// byte
byte num = 127;
// case
int day = 3;
switch (day) {
case 1:
System.out.println("Monday");
break;
case 2:
System.out.println("Tuesday");
break;
case 3:
System.out.println("Wednesday");
break;
}
// catch
try {
int a = 10 / 0;
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
// char
char ch = 'a';
// class
class Animal {
String name;
int age;
}
// continue
for (int i = 0; i < 10; i++) {
if (i == 5) {
continue;
}
System.out.println(i);
}
// default
int day = 10;
switch (day) {
case 1:
System.out.println("Monday");
break;
case 2:
System.out.println("Tuesday");
break;
default:
System.out.println("Invalid day");
}
// do
int i = 0;
do {
System.out.println(i);
i++;
} while (i < 5);
// double
double num = 3.14;
// else
int num = 10;
if (num % 2 == 0) {
System.out.println(num + " is even");
} else {
System.out.println(num + " is odd");
}
// enum
enum Direction {
UP, DOWN, LEFT, RIGHT
}
// extends
class Animal {
String name;
int age;
}
class Dog extends Animal {
String breed;
}
// final
final int MAX_NUM = 100;
// finally
try {
int a = 10 / 0;
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
} finally {
System.out.println("Finally block is executed.");
}
// float
float num = 3.14f;
// for
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
// if
int num = 10;
if (num % 2 == 0) {
System.out.println(num + " is even");
}
// implements
interface Printable {
void print();
}
class Document implements Printable {
@Override
public void print() {
System.out.println("Printing document...");
}
}
// import
import java.util.ArrayList;
// instanceof
Animal animal = new Animal();
if (animal instanceof Animal) {
System.out.println("animal is an instance of Animal");
}
// int
int num = 10;
// interface
interface Printable {
void print();
}
class Document implements Printable {
@Override
public void print() {
System.out.println("Printing document...");
}
}
// long
long num = 1000000L;
// native
public native void nativeMethod();
// new
Animal animal = new Animal();
// package
package com.example;
// private
class Animal {
private String name;
private int age;
}
// protected
class Animal {
protected String name;
protected int age;
}
// public
public class Animal {
public String name;
public int age;
}
// return
int sum(int a, int b) {
return a + b;
}
// short
short num = 100;
// static
class MathUtils {
public static int max(int a, int b) {
return a > b ? a : b;
}
}
// strictfp
strictfp class Calculator {
public strictfp double calculate(double num1, double num2) {
return num1 + num2;
}
}
// super
class Animal {
String name;
int age;
}
class Dog extends Animal {
String breed;
public void display() {
System.out.println("Name: " + name);
System.out.println("Age: " + age);
System.out.println("Breed: " + breed);
super.display();
}
}
// switch
int day = 3;
switch (day) {
case 1:
System.out.println("Monday");
break;
case 2:
System.out.println("Tuesday");
break;
case 3:
System.out.println("Wednesday");
break;
}
// synchronized
class BankAccount {
private int balance = 0;
public synchronized void deposit(int amount) {
balance += amount;
}
public synchronized void withdraw(int amount) {
balance -= amount;
}
}
// this
class Animal {
String name;
int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
// throw
try {
throw new Exception("Something went wrong.");
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
// throws
class MathUtils {
public static int divide(int a, int b) throws ArithmeticException {
if (b == 0) {
throw new ArithmeticException("Cannot divide by zero.");
}
return a / b;
}
}
// transient
class Animal implements Serializable {
String name;
transient int age;
}
// try
try {
int a = 10 / 0;
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
// void
void printMessage() {
System.out.println("Hello, world!");
}
// volatile
class Counter {
private volatile int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
// while
int i = 0;
while (i < 10) {
System.out.println(i);
i++;
}