Java核心技术
文章目录
一、Java程序设计概述
1. Java白皮书的关键术语
- 简单性
- 面向对象
- 分布式
- 健壮性
- 安全性
- 体系结构中立
- 可移植性
- 解释型
- 高性能
- 多线程
- 动态性
2. Java applet和Internet
这里的想法很简单:用户从Internet下载Java字节码,并在自己的机器上运行。在网页中运行的Java程序称为applet。要使用 applet,需要启用Java的 Web浏览器执行字节码。不需要安装任何软件。任何时候只要访问包含applet的网页都会得到程序的最新版本。最重要的是,要感谢虚拟机的安全性,它让我们不必再担心来自恶意代码的攻击。
二、Java程序设计环境
1. 安装Java开发工具包
- 下载JDK
Java术语表
术语名 | 缩写 | 解释 |
Java Development Kit | JDK | 编写Java程序的程序员使用的软件 |
Java Runtime Environment | JRE | 运行Java程序的用户使用的软件 |
Server JRE | —— | 在服务器上运行Java程序的软件 |
Standard Edition | SE | 用于桌面或简单服务器应用的Java平台 |
Enterprise Edition | EE | 用于复杂服务器应用的Java平台 |
Micro Edition | ME | 用于手机和其他小型设备的Java平台 |
Java FX | —— | 用于图形化用户界面的一个替代工具包 |
OpenJDK | —— | Java SE的一个免费开源实现,不包含浏览器集成和JavaFX |
Update | u | Oracle的术语,表示bug修正版本 |
NetBeans | —— | Oracle的集成开发环境 |
- 设置JDK
- 安装库源文件和文档
2. 使用命令行工具
3. 使用集成开发环境
4. 构建并运行applet
三、Java的基本程序设计结构
1. 一个简单的Java应用程序
public class FirstSimple {
public static void main(String args[]) {
System.out.println("Hello, Java!");
}
}
2. 注释
//
/* */
/** */
3. 数据类型
- 整数
Java整数
类型 | 大小 | 取值范围 |
---|---|---|
int | 4字节 | -2 147 483 648 ~ 2 147 483 647 |
short | 2字节 | -32 768 ~ 32 767 |
long | 8字节 | -9 223 372 036 854 775 808 ~ 9 223 372 036 854 775 807 |
byte | 1字节 | -128 ~ 127 |
- 浮点数
Java浮点数
类型 | 大小 | 取值范围 |
---|---|---|
float | 4字节 | 有效位数为6~7位 |
double | 8字节 | 有效位数为15位 |
-
char类型:大小两个字节。
-
Unicode和char类型
-
boolean类型:大小1个字节。
4. 变量
- 变量初始化
- 常量:利用关键字
final
来表示。
关键字final表示这个变量只能被赋值一次。一旦被赋值之后,就不能够再更改了。习惯上,常量名使用全大写。
public class Constant2 {
public static final double CM_PER_INCH = 2.54;
public static void main(String args[]) {
double paperWidth = 8.5;
double paperHeight = 11;
System.out.println("Paper size is :" + paperWidth * paperHeight * CM_PER_INCH * CM_PER_INCH);
}
}
5. 运算符
可使用
stricftp
来指定按照严格的浮点数计算方法来执行浮点数之间的运算。
例如:
public static stricftp void main(String args[]) { }
-
数学函数与常量
-
数值类型之间的转换
合法转换:
- 如果两个操作数中有一个是double类型,另一个操作数就会转换为double类型。
- 否则,如果其中一个操作数是float类型,另一个操作数将会转换为float类型。
- 否则,如果其中一个操作数是 long类型,另一个操作数将会转换为long类型。
- 否则,两个操作数都将被转换为int类型。
- 强制类型转换
四舍五入操作需要用到Math.round()方法。
- 结合赋值和运算符
- 自增与自减运算符
- 关系和Boolean运算符
- 位运算符
- 括号与运算符级别
Java运算符优先级
运算符 | 结合性 |
---|---|
[]、()(方法调用) | 从左向右 |
!、~、++、--、+、-、()(强制类型转换)、new | 从右向左 |
*、/、% | 从左向右 |
<<、>>、>>> | 从左向右 |
<、<=、>、>=、instanceof | 从左向右 |
==、!= | 从左向右 |
& | 从左向右 |
^ | 从左向右 |
| | 从左向右 |
&& | 从左向右 |
|| | 从左向右 |
?: | 从右向左 |
=、+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=、>>>= | 从右向左 |
- 枚举类型
6. 字符串
- 子串
String类的substring方法可以从一个较大的字符串提取出一个子串。 - 拼接
可以使用+
拼接两个字符串,也可以使用静态的join方法:String all = String.join("/", “S”, “M”, “L” ,“XL”); all = “S/M/L/XL”; - 不可变字符串
- 检测字符串是否相等:a.equals(b);或者a.equalsIgnoreCase(b);
- 空串与NULL串
- 码点与代码单元
- length()方法将返回给定字符串所需要的代码单元数量。
- codePointCount()方法将返回实际的(码点数量)的长度。
- String API
- char charAt(int index):返回给定位置的代码单元
- int codePointAt(int index):返回从给定位置开始的码点
- int offsetByCodePoints(int startIndex,int cpCount):返回从 startIndex代码点开始,位移cpCount后的码点索引。
- int compareTo(String other):按照字典顺序,如果字符串位于other 之前,返回一个负数;如果字符串位于other之后,返回一个正数;如果两个字符串相等,返回0。
- IntStream codePoints():将这个字符串的码点作为一个流返回。调用toArray将它们放在一个数组中。
- new String(int[] codePoints, int offset, int count):用数组中从 offset开始的count个码点构造一个字符串。
- boolean equals(Object other):如果字符串与other相等,返回true。
- boolean equalsIgnoreCase(Object other):如果字符串与other相等(忽略大小写),返回true。
- boolean startWith(String prefix);
- boolean endWith(String suffix):如果字符串以suffix开头或结尾,则返回 true。
- int indexOf(String str);
- int indexOf(String str, int fromIndex):
- int indexOf(int cp);
- int indexOf(int cp, int fromIndex):返回与字符串str或代码点cp 匹配的第一个子串的开始位置。这个位置从索引0或fromIndex开始计算。如果在原始串中不存在str,返回-1。
- int lastindexOf(String str);
- int lastindexOf(String str, int fromIndex):
- int lastindexOf(int cp);
- int lastindexOf(int cp, int fromIndex):返回与字符串str或代码点cp 匹配的最后一个子串的开始位置。这个位置从尾端或fromIndex开始计算。如果在原始串中不存在str,返回-1。
- int length():返回字符串的长度
- int codePointCount(int startIndex, int endIndex):返回startIndex和 endIndex-1之间的代码点数量。没有配成对的代用字符将计入代码点。
- String replace(charSequence oldString, charSequence newString):返回一个新字符串。这个字符串用newString 代替原始字符串中所有的oldString。可以用String 或StringBuilder对象作为CharSequence参数。
- String substring(int beginIndex):
- String substring(int beginIndex, int endIndex):返回一个新字符串。这个字符串包含原始字符串中从 beginIndex到串尾或endIndex-1的所有代码单元。
- String toLowerCase();
- Strinf toUpperCase():返回一个新字符串。这个字符串将原始字符串中的大写字母改为小写,或者将原始字符串中的所有小写字母改成了大写字母。
- String trim():返回一个新字符串。这个字符串将删除了原始字符串头部和尾部的空格。
- String join(CharSequence delimiter, ChaeSequence … elements):返回一个新字符串,用给定的定界符连接所有元素。
- 阅读联机API文档
- 构建字符串
- 构建一个空的字符串构建器:StringBuilder builder = new StringBuilder();
- 每次需要添加内容时调用append()方法:builder.append(str);
- 需要构建字符串时调用toString()方法:String str = builder.toString();
StringBuilder API:
- StringBuilder():构建一个空的字符串构建器
- int length():返回构建器或缓冲器中的代码单元数量
- StringBuilder append(String str);
- StringBuilder append(char c):追加一个字符串或代码单元并返回this;
- StringBuilder appendCodePoint(int cp):追加一个代码点,并将其转换为一个或两个代码单元并返回this。
- void setCharAt(int index, char c):将第index个代码单元设置为c
- StringBuilder insert(int offset, String str):
- StringBuilder insert(int offset, char c):在offset位置插入一个字符串或者代码单元并返回this。
- StringBuilder delete(int startIndex, int endIndex):在offset位置插入一个代码单元并返回this。
- String toString():返回一个与构建器或缓冲器内容相同的字符串。
7. 输入与输出
- 读取输入
- 首先需要构造一个Scanner对象,并与标准输入流System.in关联:Scanner in = new Scanner(System.in);
- 可以使用next***()方法读取需要的信息,例如nextLine()、next()(单词)、nextDouble()。
如果需要读取密码,可以使用Console类中的readPassword()方法。
Scanner API
- Scanner(InputStream in):用给定的输入流穿件Scanner对象
- String nextLine():读取下一行输入
- String next():读取下一个单词
- int nextInt():读取下一个整数
- double nextDouble():读取下一个浮点数
- boolean hasNext():检测输入中是否还有其他单词
- boolean hasNextDouble():检测是否还有其他的整数或者浮点数。
- 格式化输出
- 文件输入与输出
要想对文件进行读取,就需要一个用File对象构造一个Scanner对象,如下所示:
Scanner in = new Scanner(Paths.get("file.txt", "UTF-8"));
要想写入文件,就需要构造一个PrintWriter对象。在构造器中,只需要提供文件名:
PrintWriter out = new PrintWriter("file.txt", "UTF-8");
8. 控制流程
- 块作用域
- 条件语句:if(condition) statement;
- 循环:while(condition) statement;或者do statemet while(contition)
- 确定循环for
- 多重选择switch
case标签可以是:
- char,byte,short,int
- 枚举常量
- 字符串字面量
- 中断控制流程语句
9. 大数值
- BigInteger add(BigInteger other)
- BigInteger subtract(BigInteger other)
- BigInteger multiply(BigInterger other)
- BigInteger divide(BigInteger other)
- BigInteger mod(BigInteger other)
- int compareTo(BigInteger other):如果这个大整数与另一个大整数other相等,返回0;如果这个大整数小于另一个大整数other,返回负数;否则,返回正数。
- static BigInteger valueof(long x):返回值等于x的大整数。
10. 数组
- for each循环:for(variable : collection) statement
- 数组初始化以及匿名数组
- 数组拷贝
在Java 中,允许将一个数组变量拷贝给另一个数组变量。这时,两个变量将引用同一个数组:
深拷贝需要使用Arrays类的copyOf方法,可以用来增加数组的大小:numbers = Arrays.copyOf(numbers, 2 * numbers.lenght); - 命令行参数:main方法中的String[] args
- 数组排序
要想对数值型数组进行排序,可以使用Arrays类中的sort方法:
Arrays API:
- static String toString(type[] a):返回包含a中数据元素的字符串,这些数据元素被放在括号内,并用逗号分隔。
参数:a类型为int、long、short 、char 、 byte 、 boolean、float或double的数组。 - static type copyOf(type[] a, int lenght):
- static type copyOfRange(type[] a, int start, int end):
返回与a类型相同的一个数组,其长度为length或者end-start,数组元素为a的值。
参数:
a 类型为int 、 long、 short、char . byte、 boolean、float或 double 的数组。
start 起始下标(包含这个值)。
end 终止下标(不包含这个值)。这个值可能大于a.length。在这种情况下,结果为0或false。
length 拷贝的数据元素长度。如果 length值大于a.length,结果为О或false ;否数组中口有前面lenoth个数捉亓妻的搓值 - static int binarySearch(type[] a, type v):
- static int binarySearch(type[] a, int start, int end, type v):采用二分搜索算法查找值v。如果查找成功,则返回相应的下标值;否则,返回一个负数值r。-r-1是为保持a有序v应插入的位置。
参数:
a 类型为int、long、short、char、byte、 boolean、float或double的有序数组。
start 起始下标(包含这个值)。
end 终止下标(不包含这个值)。
v 同a的数据元素类型相同的值。 - static void fill(type[] a, type v):将数组的所有数据元素值设置为v。
- static boolean equals(type[] a, type[] b):如果两个数组大小相同,并且下标相同的元素都对应相等,返回 trueo
- 多维数组
- 不规则数组
四、对象与类
1. 面向对象程序设计概述
-
类
类(class)
是构造对象的模板或蓝图。由类构造(construct)
对象的过程称为创建类的实例(instance)
。
封装(encapsulation)
是将数据和行为组合在一个包中,并对对象的使用者隐藏了数据的实现方式。对象中的数据称为实例域( instance field)
,操纵数据的过程称为方法( method)
。对于每个特定的类实例(对象)都有一组特定的实例域值。这些值的集合就是这个对象的当前状态( state)
。无论何时,只要向对象发送一个消息,它的状态就有可能发生改变。
实现封装的关键在于绝对不能
让类中的方法直接地访问其他类的实例域。程序
仅通过对象的方法与对象数据进行交互。 -
对象
对象的三个主要特性:- 对象的行为(behavior)———可以对对象施加哪些操作,或可以对对象施加哪些方法?
- 对象的状态(state)———当施加那些方法时,对象如何响应?
- 对象标识( identity)———如何辨别具有相同行为与状态的不同对象?
-
识别类
识别类的简单规则是在分析问题的过程中寻找名词,而方法对应着动词。 -
类之间的关系
最常见的关系:- 依赖(“uses-a”)
- 聚合(“has-a”)
- 继承(“is-a”)
2. 使用预定义类
-
对象与对象变量
要想使用对象,就必须首先构造对象,并指定其初始状态。然后,对对象应用方法。
在Java程序设计语言中,使用构造器( constructor)构造新实例。 -
Java类库中的LocalDate类
- public static LocalDate now():the current date using the system clock and default time-zone, not null
- public static LocalDate of(int year, int month,int dayOfMonth):
Parameters:
year - the year to represent, from MIN_YEAR to MAX_YEAR
month - the month-of-year to represent, from 1 (January) to 12 (December)
dayOfMonth - the day-of-month to represent, from 1 to 31 - public LocalDate plusDays(long daysToAdd):a LocalDate based on this date with the days added, not null
- public LocalDate minus(TemporalAmount amountToSubtract):a LocalDate based on this date with the subtraction made, not null
-
更改器方法与访问器方法
setter
:修改对象的方法称为更改器方法(mutator method)
getter
:只访问对象而不修改对象的方法有时称为访问器方法( accessor method)。
3. 用户自定义类
-
Employee类
最常见的类定义形式:class className { field1 fiels2 ... constructor1 constructor2 ... method1 mothod2 ... }
Employee类
class Employee { private String name; private double salary; private LocalDate hireDay; public Employee(String name, double s, int year, int month, int day) { this.name = name; salary = s; hireDay = LocalDate.of(year, month, day); } public String getName(){ return this.name; } public double getSalary(){ return this.salary; } public LocalDate getHireDay() { return this.hireDay; } public void raiseSalry(double byPercent) { double raise = salary * byPercent / 100; salary += raise; } }
-
多个原文件的使用
-
剖析Employee类
- 一个构造函数
- 四个方法
- 三个属性
-
从构造器开始
-
隐式参数与显式参数
示例:在Employee类中的raiseSalary(double byPercent)
方法中,隐式参数是调用该方法的对象实例,显式参数是byPercent。 -
封装的优点
-
基于类的访问权限
方法可以访问所属类的私有特性( feature),而不仅限于访问隐式参数的私有特性。 -
私有方法
-
final
实例域
可以将实例域定义为final。构建对象时必须初始化这样的域。一个构造器执行之后,这个域的值被设置,并且在后面的操作中,不能够再对它进行修改。
final修饰符大都应用于基本(primitive)类型域,或不可变(immutable)类的域(如果类中的每个方法都不会改变其对象,这种类就是不可变的类。例如,String类就是一个不可变的类)。
4. 静态域和静态方法
-
静态域
如果将域定义为static,每个类中只有一个这样的域。而每一个对象对于所有的实例域却都有自己的一份拷贝。就相当于C++类中的static变量。
-
静态常量
public class Math { public static final double PI = 3.1415926535...; .... }
-
静态方法
可以认为静态方法是没有this
参数的方法,即没有隐式参数。
静态方法可以访问自身类中的静态域。静态方法不能访问非静态域。
下面两种情况使用静态方法:
- 一个方法不需要访问对象状态,其所需参数都是通过显式参数提供(例如:Math.pow)。
- 一个方法只需要访问类的静态域(例如:Employee.getNextId)。 -
工厂方法
详情见NumberFormat
类中的static NumberFormat getCurrencyInstance()
方法与static NumberFormat getPercentInstance()
方法。 -
main方法
main方法不对任何对象进行操作。事实上,在启动程序时还没有任何一个对象。静态的main方法将执行并创建程序所需要的对象。每一个类可以有一个main方法。这是一个常用于对类进行单元测试的技巧。
5. 方法参数
首先回顾一下在程序设计语言中有关将参数传递给方法(或函数)的一些专业术语。`按值调用( call by value)`表示方法接收的是调用者提供的值。而`按引用调用( call by reference)`表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。"按……调用"(call by)是一个标准的计算机科学术语,它用来描述各种程序设计语言(不只是Java)中方法参数的传递方式.
==**Java程序设计语言`总是`采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,特别是,方法不能修改传递给它的任何参数变量的内容。**==
- ==**一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。**==
- ==**一个方法可以改变一个对象参数的状态。**==
- ==**一个方法不能让对象参数引用一个新的对象。**==
示例:
```java
package Test;
public class ParamTest {
public static void main(String[] args) {
/* Test1:Method cann't modify numeric parameters */
System.out.println("Testing tripleValue:");
double percent = 10;
System.out.println("Before:percent=" + percent);
tripleValue(percent);
System.out.println("After:percemt=" + percent);
/* Test2:Methods can change the state of object parameters */
System.out.println("\nTesting tripleSalary:");
Employee zunnajim = new Employee("zunnajim", 10000);
System.out.println("Before:salary=" + zunnajim.getSalary());
tripleSalary(zunnajim);
System.out.println("After:salary=" + zunnajim.getSalary());
/* Test3:Methods can't attach new Objects to Object parameter */
System.out.println("\nTesting swap:");
Employee a = new Employee("zunnajim", 10000);
Employee b = new Employee("lsl", 20000);
System.out.println("Before:a=" + a.getName());
System.out.println("Before:b=" + b.getName());
swap(a, b);
System.out.println("After:a=" + a.getName());
System.out.println("After:b=" + b.getName());
}
private static void tripleValue(double percent) {
percent *= 3;
System.out.println("End Of The Method:percent=" + percent);
}
private static void tripleSalary(Employee employee) {
employee.raiseSalry(200);
System.out.println("End Of The Method:salary=" + employee.getSalary());
}
private static void swap(Employee a, Employee b) {
Employee temp = a;
a = b;
b = temp;
System.out.println("End Of The Method:x=" + a.getName());
System.out.println("End Of The Method:y=" + b.getName());
}
}
```
运行结果:
Testing tripleValue:
Before:percent=10.0
End Of The Method:percent=30.0
After:percemt=10.0
Testing tripleSalary:
Before:salary=10000.0
End Of The Method:salary=30000.0
After:salary=30000.0
Testing swap:
Before:a=zunnajim
Before:b=lsl
End Of The Method:x=lsl
End Of The Method:y=zunnajim
After:a=zunnajim
After:b=lsl
6. 对象构造
-
重载
如果多个方法(比如,StringBuilder构造器方法)有目同的名字、不同的参数,便产生了重载。编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。 -
默认域初始化
如果在构造器中没有显式地给域赋予初值,那么就会被自动地赋为默认值:数值为0、布尔值为false、对象引用为null。 -
无参数的构造器
如果在编写一个类时没有编写构造器,那么系统就会提供一个无参数构造器。这个构造器将所有的实例域设置为默认值。于是,实例域中的数值型数据设置为0、布尔型数据设置为false、所有对象变量将设置为null。 -
显式域初始化
可以在类定义中,直接将一个值赋给任何域。
例如:public class Employee { private String name = ""; .... }
-
参数名
-
调用另一个构造器
-
初始化块
调用构造器的具体处理步骤:- 所有数据域被初始化为默认值( o、false或null)。
- 按照在类声明中出现的次序,依次执行所有域初始化语句和初始化块。
- 如果构造器第一行调用了第二个构造器,则执行第二个构造器主体。
- 执行这个构造器的主体。
-
对象析构与
finalize
方法
某些对象使用了内存之外的其他资源,例如,文件或使用了系统资源的另一个对象的句柄。在这种情况下,当资源不再需要时,将其回收和再利用将显得十分重要。
可以为任何一个类添加finalize方法。finalize方法将在垃圾回收器清除对象之前调用。在实际应用中,不要依赖于使用finalize方法回收任何短缺的资源,这是因为很难知道这个方法什么时候才能够调用。
7.包
使用包的主要原因是确保类名的唯一性。
-
类的导入
- 每个类名之前添加完整的包名。
- 使用
import
语句
C++注释:C++程序员经常将import 与#include弄混。实际上,这两者之间并没有共同之处。在C++中,必须使用#include将外部特性的声明加载进来,这是因为C++编译器无法查看任何文件的内部,除了正在编译的文件以及在头文件中明确包含的文件。Java编译器可以查看其他文件的内部,只要告诉它到哪里去查看就可以了。
-
静态导入
import语句不仅可以导入类,还增加了导入静态方法和静态域的功能。 -
将类放入包中
要想将一个类放入包中,就必须将包的名字放在源文件的开头,包中定义类的代码之前。 -
包作用域
8. 类路径
- 设置类路径
最好采用-classpath(或-cp)选项指定类路径:
9. 文档注释
-
注释的插入
javadoc实用程序(utility)从下面几个特性中抽取信息:- 包
- 公有类与接口
- 公有的和受保护的构造器及方法
- 公有的和受保护的域
应该为上面几部分编写注释。注释应该放置在所描述特性的前面。注释以
/**
开始,以*/
结束。 -
类注释
类注释必须放在import语句之后,类定义之前。 -
方法注释
每一个方法注释必须放在所描述的方法之前。除了通用标记之外,还可以使用下面的标记:- @param变量描述
这个标记将对当前方法的“param”(参数)部分添加一个条目。这个描述可以占据多行,并可以使用HTML标记。一个方法的所有@param标记必须放在一起。 - @return描述
这个标记将对当前方法添加“return”(返回)部分。这个描述可以跨越多行,并可以使用HTML标记。 - @throws类描述
这个标记将添加一个注释,用于表示这个方法有可能抛出异常。
- @param变量描述
-
域注释
只需要对公有域(通常指的是静态常量)建立文档。 -
通用注释
下面的标记可以用于类文档注释中- @author姓名
这个标记将产生一个“author”(作者)条目。可以使用多个@author标记,每个@author标记对应一个作者。 - @version文本
这个标记将产生一个“version”(版本)条目。这里的文本可以是对当前版本的任何描述。
下面的标记可以用于所有的文档注释中。 - since文本
这个标记将产生一个“since”(始于)条目。这里的text可以是对引入特性的版本描述。例如,@since version 1.7.1。 - @deprecated文本
这个标记将对类、方法或变量添加一个不再使用的注释。文本中给出了取代的建议。例如,
@deprecated UsesetVisib1e(true)
instead
通过@see和@link标记,可以使用超级链接,链接到javadoc文档的相关部分或外部文档。 - @see引用
这个标记将在“see also”部分增加一个超级链接。它可以用于类中,也可以用于方法中。
- @author姓名
-
包与概述注释
可以直接将类、方法和变量的注释放置在Java源文件中,只要用/**...*/
文档注释界定就可以了。但是,要想产生包注释,就需要在每一个包目录中添加一个单独的文件。可以有如下两个选择:
1)提供一个以package.html命名的HTML文件。在标记…之间的所有文本都会被抽取出来。
2)提供一个以package-info.java命名的Java文件。这个文件必须包含一个初始的以/**...*/
界定的Javadoc注释,跟随在一个包语句之后。它不应该包含更多的代码或注释。 -
注释的抽取
- 切换到包含想要生成文档的源文件目录。如果有嵌套的包要生成文档,例如 com.horstmann.corejava,就必须切换到包含子目录com的目录(如果存在overview.html文件的话,这也是它的所在目录)。
- 如果是一个包,应该运行命令:
javadoc -d docDirectory nameOfPackage
或对于多个包生成文档,运行:
javadoc -d docDirectory nameOfPackage1 nameOfPackage2…
如果文件在默认包中,就应该运行:
javadoc -d docDirectory *.java
如果省略了-d docDirectory选项,那 HTML文件就会被提取到当前目录下。这样有可能会带来混乱,因此不提倡这种做法。
可以使用多种形式的命令行选项对javadoc程序进行调整。
10. 类设计技巧
- 一定要保证数据私有
- 一定要对数据进行初始化
- 不要在类中过多的使用基本类型
- 不是所有的域都需要独立的域访问器和域更改器
- 将职责过多的类进行分解
- 将职责过多的类进行分解
- 优先使用不可变的类
五、 继承
1. 类、超类、子类
-
定义子类
关键字extends
表示继承。C++注释:Java与C++定义继承类的方式十分相似。Java用关键字extends代替了C++中的冒号(😃。在Java中,所有的继承都是公有继承,而没有C++中的私有继承和保护继承。
关键字
extends
表明正在构造的新类派生于一个已存在的类。已存在的类称为超类superclass)
、基类( base class)
或父类( parent class)
;新类称为子类( subclass)
、派生类(derived class)
或孩子类(child class)
。 -
覆盖方法
class Manager extends Employee { private double Bonus; public Manager(String name, double s, int year, int month, int day, double bonus) { super(name, s, year, month, day); Bonus = bonus; } public Manager(String name, double salary) { super(name, salary); } public double getSalary() { double baseSalary = super.getSalary(); return Bonus + baseSalary; }
有些人认为super 与 this引用是类似的概念,实际上,这样比较并不太恰当。这是因为super不是一个对象的引用,不能将super赋给另一个对象变量,它只是一个指示编译器调用超类方法的特殊关键字。
C++注释:在Java中使用关键字super调用超类的方法,而在C++中则采用超类名加上:操作符的形式。例如,在 Manager类的getSalary方法中,应该将super.getSalary 替换为Employee::getSalary。 -
子类构造器
一个对象变量(例如,变量e)可以指示多种实际类型的现象被称为多态( polymorphism)
。在运行时能够自动地选择调用哪个方法的现象称为动态绑定( dynamic binding)
。 -
继承层次
-
多态
父类对象可以引用子类对象警告:在Java 中,子类数组的引用可以转换成超类数组的引用,而不需要采用强制类型转换。例如,下面是一个经理数组
Manager[] managers = new Manager[10];
将它转换成Employee[]数组完全是合法的:
Employee[] staff = managers;// OK
这样做肯定不会有问题,请思考一下其中的缘由。毕竟,如果manager[i]是一个Manager,也一定是一个 Employee。然而,实际上,将会发生一些令人惊讶的事情。要切记managers和staff引用的是同一个数组。现在看一下这条语句:
staff[0] = new Employee(“Harry Hacker”,. . .);
编译器竟然接纳了这个赋值操作。但在这里,staff[0]与manager[0]引用的是同一个对象,似乎我们把一个普通雇员擅自归入经理行列中了。这是一种很忌讳发生的情形,当调用managers[0].setBonus(1000)的时候,将会导致调用一个不存在的实例域,进而搅乱相邻存储空间的内容。
为了确保不发生这类错误,所有数组都要牢记创建它们的元素类型,并负责监督仅将类型兼容的引用存储到数组中。例如,使用new managers[10]创建的数组是一个经理数组。
如果试图存储一个 Employee类型的引用就会引发ArrayStoreException异常。 -
理解方法调用
弄清楚如何在对象上应用方法调用非常重要。下面假设要调用x.f(args),隐式参数x声明为类C的一个对象。下面是调用过程的详细描述:- 编译器查看对象的声明类型和方法名。假设调用x.f(param),且隐式参数x声明为C类的对象。需要注意的是:有可能存在多个名字为f,但参数类型不一样的方法。例如,可能存在方法 f(int)和方法 f(String)。编译器将会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法(超类的私有方法不可访问)。
至此,编译器已获得所有可能被调用的候选方法。 - 接下来,编译器将查看调用方法时提供的参数类型。如果在所有名为f的方法中存在一个与提供的参数类型完全匹配,就选择这个方法。这个过程被称为重载解析( overloadingresolution)。例如,对于调用x.f(“Hello”)来说,编译器将会挑选f(String),而不是f(int)。
由于允许类型转换 ( int可以转换成double,Manager可以转换成Employee,等等),所以这个过程可能很复杂。如果编译器没有找到与参数类型匹配的方法,或者发现经过类型转换后有多个方法与之匹配,就会报告一个错误。
至此,编译器已获得需要调用的方法名字和参数类型。 - 如果是private方法、static方法、final方法(有关final修饰符的含义将在下一节讲述)或者构造器,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定( static binding)。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。在我们列举的示例中,编译器采用动态绑定的方式生成一条调用f(String)的指令。
- 当程序运行,并且采用动态绑定调用方法时,虚拟机一定调用与x所引用对象的实际类型最合适的那个类的方法。假设x的实际类型是D,它是C类的子类。如果D类定义了方法 f(String),就直接调用它;否则,将在D类的超类中寻找f(String),以此类推。
每次调用方法都要进行搜索,时间开销相当大。因此,虚拟机预先为每个类创建了一个方法表( method table)
,其中列出了所有方法的签名和实际调用的方法。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。
动态绑定有一个非常重要的特性:无需对现存的代码进行修改,就可以对程序进行扩展。
- 编译器查看对象的声明类型和方法名。假设调用x.f(param),且隐式参数x声明为C类的对象。需要注意的是:有可能存在多个名字为f,但参数类型不一样的方法。例如,可能存在方法 f(int)和方法 f(String)。编译器将会一一列举所有C类中名为f的方法和其超类中访问属性为public且名为f的方法(超类的私有方法不可访问)。
-
阻止继承:final类和方法
有时候,可能希望阻止人们利用某个类定义子类。不允许扩展的类被称为final类。public final class Excutive extends Manager { public final String getName() { } }
-
强制类型转换
进行类型转换的唯一原因是:在暂时忽视对象的实际类型之后,使用对象的全部功能。
在进行类型转换之前,先查看一下是否能够成功地转换。
使用isinstanceof
操作符实现
综上所述:- 只能在继承层次内进行类型转换。
- 在将超类转换成子类之前,应该使用instanceof进行检查。
C++注释:Java使用的类型转换语法来源于C语言“以往糟糕的日子”,但处理过程却有些像C++的dynamic_cast 操作。例如,
Manager boss = (Manager) staff[1];// Java
等价于
Manager* boss = dynamic_cast<Manager*>(staff[1]);1/C++
它们之间只有一点重要的区别:当类型转换失败时,Java不会生成一个null对象,而是抛出一个异常。从这个意义上讲,有点像C++中的引用( reference)转换。真是令人生厌。在C++中,可以在一个操作中完成类型测试和类型转换。
Manager* boss = dynamic_cast<Manager*>(staff[1]);// C++
if (boss != NULL). . .
而在Java中,需要将instanceof运算符和类型转换组合起来使用:
if (staff[1] instanceof Manager)
{
Manager boss = (Manager) staff[1];
} -
抽象类
使用abstract
关键字的方法被称为抽象方法,不需要在基类中提供实现。
为了提高程序的清晰度,包含一个或多个抽象方法的类本身必须被声明为抽象的。
抽象方法充当着占位的角色,它们的具体实现在子类中。扩展抽象类可以有两种选择。一种是在抽象类中定义部分抽象类方法或不定义抽象类方法,这样就必须将子类也标记为抽象类;另一种是定义全部的抽象方法,这样一来,子类就不是抽象的了。
类即使不含抽象方法,也可以将类声明为抽象类。
抽象类不能被实例化。也就是说,如果将一个类声明为abstract,就不能创建这个类的对象。 -
受保护访问
受保护的方法更具有实际意义。如果需要限制某个方法的使用,就可以将它声明为protected。这表明子类(可能很熟悉祖先类)得到信任,可以正确地使用这个方法,而其他类则不行。- 仅对本类可见——private
- 对所有类可见——public
- 对本包和所有子类可见——protected
- 对本包可见——默认,不需要修饰符
2. Object:所有类的超类
可以使用Object类型的变量引用任何类型的对象:
object obj = new Employee("Harry Hacker"35000);
当然,Object类型的变量只能用于作为各种值的通用持有者。要想对其中的内容进行具体的操作,还需要清楚对象的原始类型,并进行相应的类型转换:
Employee e = (Employee) obj;
在Java中,只有基本类型((primitive types)不是对象,例如,数值、字符和布尔类型的值都不是对象。
所有的数组类型,不管是对象数组还是基本类型的数组都扩展了Object类。
-
equals方法
Object类中的equals方法用于检测一个对象是否等于另外一个对象。在Object类中,这个方法将判断两个对象是否具有相同的引用。如果两个对象具有相同的引用,它们一定是相等的。从这点上看,将其作为默认操作也是合乎情理的。然而,对于多数类来说,这种判断并没有什么意义。例如,采用这种方式比较两个PrintStream对象是否相等就完全没有意义。然而,经常需要检测两个对象状态的相等性,如果两个对象的状态相等,就认为这两个对象是相等的。
public class Employee { ... public boolean equals(Object object) { if (this == object) return true; if (object == null) return false; if (getClass() != object.getClass()) return false; Employee otherEmployee = (Employee) object; return name.equals(otherEmployee.name) && salary == otherEmployee.salary && hireDay.equals(otherEmployee.hireDay); } }
-
相等测试与继承
Java语言规范要求equals方法具有下面的特性:- 自反性:对于任何非空引用x,x.equals(x)应该返回true。
- 对称性:对于任何引用x和y,当且仅当y.equals(x)返回true,x.equals(y)也应该返回true。
- 传递性: y对于任何引用x、和z,如果x.equals(y)返回 true,y.equals(z)返回 true,x.equals(z)也应该返回true。
- 一致性:如果x和y引用的对象没有发生变化,反复调用x.equals(y)应该返回同样的结果。
- 对于任意非空引用x,x.equals(null)应该返回false。
下面给出编写一个完美的equals方法的建议:
- 显式参数命名为otherObject,稍后需要将它转换成另一个叫做other的变量。
- 检测 this与 otherObject是否引用同一个对象:
if (this = otherObject) return true;
这条语句只是一个优化。实际上,这是一种经常采用的形式。因为计算这个等式要比一个一个地比较类中的域所付出的代价小得多。 - 检测otherObject是否为null,如果为null,返回false。这项检测是很必要的。
if (otherObject mm null) return false; - 比较this与otherObject是否属于同一个类。如果equals的语义在每个子类中有所改变,就使用getClass检测:
if (getClass() != otherObject.getClass(0) return false;
如果所有的子类都拥有统一的语义,就使用instanceof检测:
if (! (otherObject instanceof ClassName)) return false; - 将otherObject转换为相应的类类型变量:
ClassName other = (ClassName) otherObject
现在开始对所有需要比较的域进行比较了。使用==比较基本类型域,使用equals 比较对象域。如果所有的域都匹配,就返回true;否则返回 false。
return field1 == other.field1
&&0bjects.equals(field2,other.field2)&. …;
3. hashCode方法
如果重新定义equals方法,就必须重新定义hashCode方法。
public int hashCode() {
return Objects.hash(name,salary,hireDay);
}
4. toString方法
绝大多数(但不是全部)的 toString方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。下面是Employee类中的 toString方法的实现:
```java
@Override
public String toString() {
return "Employee [hireDay=" + hireDay + ", name=" + name + ", salary=" + salary + "]";
}
```
### 3. 泛型数组列表
ArrayList是一个采用`类型参数( type parameter)`的`泛型类( generic class)`。
> C++注释:ArrayList类似于C++的vector模板。ArrayList与vector都是泛型类型。但是C++的vector模板为了便于访问元素重载了[ ]运算符。由于Java没有运算符重载,所以必须调用显式的方法。此外,C++向量是值拷贝。如果a和b是两个向量,赋值操作a= b将会构造一个与b长度相同的新向量a,并将所有的元素由b拷贝到a,而在Java中,这条赋值语句的操作结果是让a和b引用同一个数组列表。
1. 访问数组列表元素
使用`get()`和`set()`方法来访问和修改元素。
2. 类型化与原始数组列表的兼容性
鉴于兼容性的考虑,编译器在对类型转换进行检查之后,如果没有发现违反规则的现象,就将所有的类型化数组列表转换成原始ArrayList对象。在程序运行时,所有的数组列表都是一样的,即没有虚拟机中的类型参数。因此,类型转换(ArrayList)和 (ArrayList<Employee>)将执行相同的运行时检查。
在这种情形下,不必做什么。只要在与遗留的代码进行交叉操作时,研究一下编译器的警告性提示,并确保这些警告不会造成太严重的后果就行了。
一旦能确保不会造成严重的后果,可以用@SuppressWarnings("unchecked")标注来标记这个变量能够接受类型转换,如下所示:
@Suppresswarnings("unchecked") ArrayList<Employee> result=(ArrayList<Employee>) employeeDB.find(query);
// yields another warning
### 4. 对象包装器与自动装箱
有时,需要将int这样的基本类型转换为对象。所有的基本类型都有一个与之对应的类。例如,Integer类对应基本类型 int。通常,这些类称为包装器( wrapper)。这些对象包装器类拥有很明显的名字: Integer 、 Long 、 Float、Double 、 Short、Byte , Character 、Void和 Boolean(前6个类派生于公共的超类Number)。对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。同时,对象包装器类还是final,因此不能定义它们的子类。
- 自动装箱:
list.add(3)自动变换成list.add(Integer.valueof(3));
- 自动拆箱:
int n = list.get(i)自动变化成int n = list.get(i).intVlue();
最后强调一下,装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行这些字节码。
### 5. 参数数量可变的方法
printf方法的定义:
```java
public class PrintStream {
public PrintStream printf(String fmt, Object ... args);
}
甚至可以将main方法定义为:
public static void main(String … args) {
}
6. 枚举类
- static Enum valueOf(Class enumClass, String name):返回指定名字、给定类的枚举常量。
- String toString():返回枚举常量名
- int ordinal():返回枚举常量在enum声明中的位置,位置从0开始计数。
- int compareTo(E other):如果枚举常量出现在other之前,则返回一个负值;如果 this==other,则返回0;否则,返回正值。枚举常量的出现次序在enum声明中给出。
7. 反射
反射库( reflection library)提供了一个非常丰富且精心设计的工具集,以便编写能够动态操纵Java代码的程序。这项功能被大量地应用于JavaBeans 中,它是Java 组件的体系结构。使用反射,Java可以支持Visual Basic用户习惯使用的工具。特别是在设计或运行中添加新类时,能够快速地应用开发工具动态地查询新添加类的能力。
能够分析类能力的程序称为反射(( reflective)。反射机制的功能极其强大,在下面可以看到,反射机制可以用来:
- 在运行时分析类的能力。
- 在运行时查看对象,例如,编写一个toString方法供所有类使用。
- 实现通用的数组操作代码。
- 利用Method对象,这个对象很像C++中的函数指针。
-
Class类
在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识。这个信息跟踪着每个对象所属的类。虚拟机利用运行时类型信息选择相应的方法执行。常用方法:
- getName():
employee.getClass().getName():Test.Employee.Employee - 静态方法:forName():
String className = “java.util.Random”;
Class class = Class.forName(calssName); - 如果T是任意的Java类型,T.class将代表匹配的类对象:
Class class1 = Random.class;
Class class2 = int.class;
Class class3 = Double[].class; - newInstance():
String className = “java.util.Random”;
Obejct obj = Class.forName(className).newInstance();
C++注释:newInstance方法对应C++中虚拟构造器的习惯用法。然而,C++中的虚拟构造器不是一种语言特性,需要由专门的库支持。Class类与C++中的type_info类相似,getClass方法与C++中的typeid运算符等价。但Java中的Class 比 C++中的type_info的功能强。C++中的type_info只能以字符串的形式显示一个类型的名字,而不能创建那个类型的对象。
- getName():
-
捕获异常
当程序运行过程中发生错误时,就会“抛出异常”。抛出异常比终止程序要灵活得多,这是因为可以提供一个“捕获”异常的处理器(handler)对异常情况进行处理。
如果没有提供处理器,程序就会终止,并在控制台上打印出一条信息,其中给出了异常的类型。
异常有两种类型:未检查异常
和已检查异常
。对于已检查异常
,编译器将会检查是否提供了处理器。然而,有很多常见的异常,例如,访问null引用,都属于未检查异常
。编译器不会查看是否为这些错误提供了处理器。毕竟,应该精心地编写代码来避免这些错误的发生,而不要将精力花在编写异常处理器上。 -
利用反射分析类的能力
在java.lang.reflect包中有三个类Field、Method和 Constructor分别用于描述类的域、方法和构造器。这三个类都有一个叫做getName的方法,用来返回项目的名称。Field类有一个getType方法,用来返回描述域所属类型的Class对象。Method和Constructor类有能够报告参数类型的方法,Method类还有一个可以报告返回类型的方法。这三个类还有一个叫做getModifiers 的方法,它将返回一个整型数值,用不同的位开关描述public 和 static这样的修饰符使用状况。另外,还可以利用java.lang.reflect包中的Modifier类的静态方法分析getModifiers返回的整型数值。例如,可以使用Modifier类中的 isPublic、isPrivate或isFinal判断方法或构造器是否是public、 private或final。我们需要做的全部工作就是调用Modifier类的相应方法,并对返回的整型数值进行分析,另外,还可以利用Modifier.toString方法将修饰符打印出来。
Class类中的getFields、 getMethods和 getConstructors方法将分别返回类提供的public域、方法和构造器数组,其中包括超类的公有成员。Class类的getDeclareFields ,getDeclareMethods和 getDeclaredConstructors方法将分别返回类中声明的全部域、方法和构造器,其中包括私有和受保护成员,但不包括超类的成员。
package Test;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;
public class ReflectionTest {
public static void main(String[] args) {
String name;
if (args.length > 0)
name = args[0];
else {
Scanner in = new Scanner(System.in);
System.out.println("Please Enter Class Name(e.g. java.util.Random):");
name = in.next();
}
try {
Class class1 = Class.forName(name);
Class superClass = class1.getSuperclass();
String modifiers = Modifier.toString(class1.getModifiers());
if (modifiers.length() > 0)
System.out.print(modifiers + " ");
System.out.print("class " + name);
if (superClass != null && superClass != Object.class)
System.out.print(" extends " + superClass.getName());
System.out.print("\n{\r");
printConstructors(class1);
System.out.println();
printMethods(class1);
System.out.println();
printFields(class1);
System.out.println("}");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.exit(0);
}
private static void printConstructors(Class class1) {
Constructor[] constructors = class1.getDeclaredConstructors();
for (Constructor constructor : constructors) {
String name = constructor.getName();
System.out.print("\t");
String modifiers = Modifier.toString(constructor.getModifiers());
if (modifiers.length() > 0)
System.out.print(modifiers + " ");
System.out.print(name + "(");
Class[] paramTypes = constructor.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
if (i > 0)
System.out.print(", ");
System.out.print(paramTypes[i].getName());
}
System.out.println(");");
}
}
private static void printMethods(Class class1) {
Method[] methods = class1.getDeclaredMethods();
for (Method method : methods) {
Class retType = method.getReturnType();
String name = method.getName();
System.out.print("\t");
String modifiers = Modifier.toString(method.getModifiers());
if(modifiers.length()>0)
System.out.print(modifiers+" ");
System.out.print(retType.getName()+name+"(");
Class[] paramTypes = method.getParameterTypes();
for (int i = 0; i < paramTypes.length; i++) {
if (i > 0)
System.out.print(", ");
System.out.print(paramTypes[i].getName());
}
System.out.println(");");
}
}
private static void printFields(Class class1) {
Field[] fields = class1.getDeclaredFields();
for (Field field : fields) {
Class type = field.getType();
String name = field.getName();
System.out.print("\t");
String modifiers = Modifier.toString(field.getModifiers());
if (modifiers.length() > 0)
System.out.print(modifiers + " ");
System.out.println(type.getName() + name + ";");
}
}
}
-
运行时使用反射分析对象
查看对象域的关键方法是Field类中的get方法。如果f是一个Field类型的对象(例如,通过getDeclaredFields得到的对象),obj是某个包含f域的类的对象,f.get(obj)将返回一个对象,其值为obj域的当前值。Employee harry = new Employee("Harry Hacker",3500, 10, 1, 1989); Class class1 = harry.getClass(); Field field = class1.getDeclaredField("name"); field.setAccessible(true); Obeject obj = field.get(harry);
-
使用反射机制编写泛型数组代码
public static Object goodCopyOf(Obejct a, int newLength) { Class cl = a.getClass(); if(!cl.isArray()) return null; Class componentType = cl.getComponentType(); int length = Array.getLength(a); Object newArray = Array.newInstance(componentType, newLength); System.arraycopy(a, 0, 0, Math.min(length, newLength)); }
-
调用任意方法
Method类中有一个invoke
方法,它允许调用包装在当前Method对象中的方法。invoke
方法的签名是
Object invoke(Object obj, Object … args);
获取Method对象的方法:
Method m1 = Employee.class.getMethod("getName);
Method m2 = Employee.class.getMethod(“raiseSalary”, double.class);
8. 继承的设计技巧
- 将公共操作和域放在超类
- 不要使用受保护的域
- 使用继承实现"is-a"关系
- 除非所有继承的方式都有意义,否则不要使用继承
- 在覆盖方法时,不要改变预期的行为
- 使用多态,而非类型信息
- 不要过多的使用反射
六、接口、lambda表达式与内部类
1. 接口
-
接口概念
在Java程序设计语言中,接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
接口中的所有方法自动地属于public。因此,在接口中声明方法时,不必提供关键字public。
当然,接口中还有一个没有明确说明的附加要求:在调用x.compareTo(y)的时候,这个compareTo方法必须确实比较两个对象的内容,并返回比较的结果。当x小于y时,返回一个负数;当x等于y时,返回0;否则返回一个正数。
为了让类实现一个接口,通常需要下面两个步骤:- 将类声明为实现给定的接口。
- 对接口中的所有方法进行定义。
要将类声明为实现某个接口,需要使用关键字implements:
class Employee implements Comparabel
Java程序设计语言是一种强类型( strongly typed)语言。在调用方法的时候,编译器将会检查这个方法是否存在。
public class Employee implements Comparable<Employee> { @Override public int compareTo(Employee other) { return Double.compare(salary, other.salary); } }
-
接口特性
接口不是类,尤其不能使用new运算符实例化一个接口:
x = new Comparable(. . .);// ERROR
然而,尽管不能构造接口的对象,却能声明接口的变量:
Comparable x;// OK
接口变量必须引用实现了接口的类对象:
x = new Employee(. . .);// oK provided Employee implements Comparable
接下来,如同使用instanceof检查一个对象是否属于某个特定类一样,也可以使用instance检查一个对象是否实现了某个特定的接口:
if (anObject instanceof Comparable){ . . .} -
接口与抽象类
继承只能继承一个类,无法多继承(广度),但是一个类可以实现多个接口。 -
静态方法
在Java SE 8中,允许在接口中增加静态方法。理论上讲,没有任何理由认为这是不合法的。只是这有违于将接口作为抽象规范的初衷。 -
默认实现方法
可以为接口方法提供一个默认实现。必须用default修饰符标记这样一个方法。public interface Comparable<T> { default int compareTo(T other) { return 0; } // By default,al1 elements are the same }
默认方法的一个重要用法是“接口演化”( interface evolution)。
为接口增加一个非默认方法不能保证“源代码兼容”(source compatible)。 -
解决默认方法冲突
解决规则如下:- 超类优先。如果超类提供了一个具体方法,(接口中)同名而且有相同参数类型的默认方法会被忽略。
- 接口冲突。如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。
2. 接口示例
-
接口与回调
回调(callback)
程序设计模式可以指出某个待定事件发生时应该采取的动作。 -
Comparator接口
比较器是实现了Comparator接口的类的实例:public interface Comparator<T> { int compare(T first, T second); }
-
对象克隆
对于每一个类,需要确定:- 默认的clone方法是否满足需求;
- 是否可以在可变的子对象上调用clone来修补默认的clone方法;
- 是否不该使用clone;
满足前两者,类必须:
- 实现Cloneable接口;
- 重新定义clone方法,并指定public修饰符。
3. lambda表达式
-
lambda表达式语法
lambda表达式其实就是一个代码块,以及必须传入代码的变量规范:(String first, String second) -> first.lebght() - second.length();
-
函数式接口
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口(functional interface)
。为了展示如何转换为函数式接口,下面考虑Arrays.sort方法。它的第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口,所以可以提供一个lambda表达式:
Arrays.sort(words, (first, second) -> first.length()- second.length());
-
方法引用
- object::intanceMethod
- Class::staticMethod
- Class::intanceMethod
-
构造器引用
使用方式:Class::new
-
变量作用域
-
处理lambda表达式
4. 内部类
`内部类(inner class)`是定义在另一个类中的类。
原因:
- 内部类方法可以访问该类定义所在的作用域中的数据,包括私有的数据。
- 内部类可以对同一个包中的其他类隐藏起来。
- 当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous)内部类比较便捷。
好处:
- 命名控制
- 访问控制。
- 内部类的对象有一个隐式引用,它引用了实例化该内部对象的外围类对象。
-
使用内部类访问对象状态
内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。
内部类的对象总有一个隐式引用,他指向了创建它的外部类对象。package Test; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.Date; import javax.swing.JOptionPane; import javax.swing.Timer; public class InnerClassTest { public static void main(String[] args) { TalkingClock clock = new TalkingClock(1000, true); clock.start(); JOptionPane.showMessageDialog(null, "Quit program"); System.exit(0); } } class TalkingClock { private int interval; private boolean beep; public TalkingClock(int interval, boolean beep) { this.interval = interval; this.beep = beep; } public void start() { ActionListener listener = new TimePrinter(); Timer timer = new Timer(interval,listener); timer.start(); } public class TimePrinter implements ActionListener { @Override public void actionPerformed(ActionEvent e) { System.out.println("At the tone, the time is " + new Date()); if (beep) Toolkit.getDefaultToolkit().beep(); } } }
-
内部类的特殊语法规则
表达式
OuterClass.this
表示外围类引用。
明确编写内部对象的构造器:
outerObject.new InnerClass(Contruction Paameters);
示例:
ActionListener listener = this.new TimePrinter();在外围类的作用域外,可以这样引用内部类:
OuterClass.InnerClass内部类中声明的所有静态域都必须是final。
内部类不能有static方法。 -
内部类是否有用、必要、安全
-
局部内部类
局部类不能用public或 privatc访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。
局部类有一个优势,即对外部世界可以完全地隐藏起来。即使TalkingClock类中的其他代码也不能访问它。除start方法之外,没有任何方法知道TimePrinter类的存在。public void start() { class TimePrinter implements ActionListener { @Override public void actionPerformed(ActionEvent e) { System.out.println("At the tone, the time is " + new Date()); System.out.println(getClass()); if (beep) Toolkit.getDefaultToolkit().beep(); } } ActionListener listener = new TimePrinter(); Timer timer = new Timer(interval,listener); timer.start(); }
-
由外部方法访问变量
与其他内部类相比较,局部类还有一个优点。它们不仅能够访问包含它们的外部类,还可以访问局部变量。不过,那些局部变量必须事实上为final。这说明,它们一旦赋值就绝不会改变。 -
匿名内部类
只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类(anonymous inner class)
;public void start() { ActionListener listener = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { System.out.println("At the tone, the time is " + new Date()); System.out.println(getClass()); if (beep) Toolkit.getDefaultToolkit().beep(); } } Timer timer = new Timer(interval,listener); timer.start(); }
通常的语法格式:
new SuperType(construction parameters) {
inner class mthods and data
}其中,SuperType可以是ActionListener这样的接口,于是内部类就要实现这个接口。SuperType也可以是一个类,于是内部类就要扩展它。
由于构造器的名字必须与类名相同,而匿名类没有类名,所以,匿名类不能有构造器。取而代之的是,将构造器参数传递给超类(superclass)构造器。尤其是在内部类实现接口的时候,不能有任何构造参数。不仅如此,还要像下面这样提供一组括号:
new InterfaceType(){
methods and data
}如果构造参数的闭小括号后面跟一个开大括号,正在定义的就是匿名内部类。
对匿名子类使用getClass()方法通常会失败
-
静态内部类
有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为static,以便取消产生的引用。
当然,只有内部类可以声明为static。静态内部类的对象除了没有对生成它的外围类对象的引用特权外,与其他所有内部类完全一样。在我们列举的示例中,必须使用静态内部类,这是由于内部类对象是在静态方法中构造的:注释:在内部类不需要访问外围类对象的时候,应该使用静态内部类。有些程序员用嵌套类(nested class)表示静态内部类。
注释:与常规内部类不同,静态内部类可以有静态域和方法。
注释:声明在接口中的内部类自动成为static和public类。package Test.InnerClass; public class StaticInnerClass { public static void main(String[] args) { double[] dArray = new double[20]; for (int i = 0; i < dArray.length; i++) { dArray[i] = 100 * Math.random(); } ArrayAlg.Pair pair = ArrayAlg.minmax(dArray); System.out.println(pair); } } class ArrayAlg { public static class Pair { private double first; private double second; public Pair(double first, double second) { this.first = first; this.second = second; } public double getFirst() { return first; } public double getSecond() { return second; } @Override public String toString() { return "Pair [minValue=" + first + ", maxVlue=" + second + "]"; } } public static Pair minmax(double[] values) { double min = Double.POSITIVE_INFINITY; double max = Double.NEGATIVE_INFINITY; for (double d : values) { if (min > d) min = d; if (max < d) max = d; } return new Pair(min, max); } }
5. 代理
利用代理可以在运行时创建一个实现了一组给定接口的新类。这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。
-
何时使用代理
假设有一个表示接口的Class对象(有可能只包含一个接口),它的确切类型在编译时无法知道。这确实有些难度。要想构造一个实现这些接口的类,就需要使用newInstance方法或反射找出这个类的构造器。但是,不能实例化一个接口,需要在程序处于运行状态时定义一个新类。
为了解决这个问题,有些程序将会生成代码;将这些代码放置在一个文件中;调用编译器;然后再加载结果类文件。很自然,这样做的速度会比较慢,并且需要将编译器与程序放在一起。而代理机制则是一种更好的解决方案。代理类可以在运行时创建全新的类。这样的代理类能够实现指定的接口。尤其是,它具有下列方法:- 指定接口所需要的全部方法。
- Object类中的全部方法,例如,toString、equals 等。
然而,不能在运行时定义这些方法的新代码。而是要提供一个调用处理器( invocationhandler)
。调用处理器是实现了InvocationHandler
接口的类对象。在这个接口中只有一个方法:
Object invoke(Object proxy,Method method,)Object[] args)
无论何时调用代理对象的方法,调用处理器的invoke方法都会被调用,并向其传递Method对象和原始的调用参数。调用处理器必须给出处理调用的方式。 -
创建代理对象
要想创建一个代理对象,需要使用Proxy类的newProxyInstance方法。这个方法有三个参数:- 一个类加载器( class loader)。作为Java安全模型的一部分,对于系统类和从因特网上下载下来的类,可以使用不同的类加载器。目前,用null表示使用默认的类加载器。
- 一个 Class对象数组,每个元素都是需要实现的接口。
- 一个调用处理器。
package Test; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.Random; public class ProxyTest { public static void main(String[] args) { Object[] elements = new Object[1000]; for (int i = 0; i < elements.length; i++) { Integer value = i + 1; InvocationHandler handler = new TraceHandeler(value); // 第二个参数是需要实现的接口 Object proxy = Proxy.newProxyInstance(null, new Class[] { Comparable.class }, handler); elements[i] = proxy; } Integer key = new Random().nextInt(elements.length) + 1; int result = Arrays.binarySearch(elements, key); if (result > 0) System.out.println(elements[result]); } } class TraceHandeler implements InvocationHandler { private Object target; public TraceHandeler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.print(target); System.out.print("." + method.getName() + "("); if (args != null) { for (int i = 0; i < args.length; i++) { System.out.print(args[i]); if (i < args.length - 1) System.out.print(", "); } } System.out.println(")"); //method是Compareable接口的方法 return method.invoke(target, args); } }
-
代理类的特性
代理类是在程序运行过程中创建的。然而,一旦被创建,就变成了常规类,与虚拟机中的任何其他类没有什么区别。
所有的代理类都扩展于Proxy类。一个代理类只有一个实例域——调用处理器,它定义在Proxy 的超类中。为了履行代理对象的职责,所需要的任何附加数据都必须存储在调用处理器中。例如,在程序清单6-10给出的程序中,代理 Comparable对象时,TraceHandler包装了实际的对象。
没有定义代理类的名字,Sun 虚拟机中的Proxy类将生成一个以字符串SProxy开头的类名。
对于特定的类加载器和预设的一组接口来说,只能有一个代理类。也就是说,如果使用同一个类加载器和接口数组调用两次newProxyInstance方法的话,那么只能够得到同一个类的两个对象,也可以利用getProxyClass方法获得这个类:
Class proxyClass = Proxy.getProxyClass(nul1,interfaces);
代理类一定是public和 final。如果代理类实现的所有接口都是public,代理类就不属于某个特定的包;否则,所有非公有的接口都必须属于同一个包,同时,代理类也属于这个包。
可以通过调用Proxy类中的 isProxyClass方法检测一个特定的Class对象是否代表一个代理类。
七、异常、断言和日志
1. 处理错误
如果由于出现错误而使得某些操作没有完成,程序应该:
- 返回到一种安全状态,并能够让用户执行一些其他的命令;
- 或者允许用户保存所有操作的结果,并以妥善的方式终止程序。
要做到这些并不是一件很容易的事情。其原因是检测(或引发)错误条件的代码通常离那些能够让数据恢复到安全状态,或者能够保存用户的操作结果,并正常地退出程序的代码很远。异常处理的任务就是将控制权从错误产生的地方转移给能够处理这种情况的错误处理器。为了能够在程序中处理异常情况,必须研究程序中可能会出现的错误和问题,以及哪类问题需要关注。
1. 用户输入错误
2. 设备错误
3. 物理限制
4. 代码错误
-
异常分类
需要注意的是,所有的异常都是由Throwable
继承而来,但在下一次立即分为两个分支:Error
和Exception
.
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。
Exception
层次结构又分解为两个分支:- 派生于
RuntimeException
- 其他异常
划分两个分支的规则是:由程序错误导致的异常属于RuntimeException ;而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
派生于
RuntimeExceotion
的异常包括以下这几种情况:- 错误的类型转换。
- 数组访问越界。
- 访问null 指针。
不是派生于RuntimeException
的异常包括: - 试图在文件尾部后面读取数据。
- 试图打开一个不存在的文件。
- 试图根据给定的字符串查找Class对象,而这个字符串表示的类并不存在。
Java语言规范将派生于
Error
类或RuntimeException
类的所有异常称为非受查unchecked)异常
,所有其他的异常称为受查( checked))异常
。 - 派生于
-
声明受查异常
方法应该在其首部声明所有可能抛出的异常。
什么时候抛出异常,什么异常必须抛出:- 调用一个抛出受查异常的方法,例如,FileInputStream构造器。
- 程序运行过程中发现错误,并且利用throw语句抛出一个受查异常。
- 程序出现错误,例如,a[-1]=0会抛出一个ArrayIndexOutOfBoundsException这样的非受查异常。
- Java 虚拟机和运行时库出现的内部错误。
总之,一个方法必须声明所有可能抛出的受查异常,而非受查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)。如果方法没有声明所有可能发生的受查异常,编译界献会发出一个错误消息
C++注释:Java中的 throws 说明符与C++中的throw说明符基本类似,但有一点重要的区别。在C++中,throw说明符在运行时执行,而不是在编译时执行。也就是说,C++编译器将不处理任何异常规范。但是,如果函数抛出的异常没有出现在throw列表中,就会调用unexpected函数,这个函数的默认处理方式是终止程序的执行。
另外,在C++中,如果没有给出 throw说明,函数可能会抛出任何异常。而在Java中,没有throws说明符的方法将不能抛出任何受查异常。 -
如何抛出异常
抛出异常的语句:String readData(Scanner in) throws EOFException { while(...) { if(!in.hasNext()) { if(n < lenght) { throw new EOFException(); } } } return s; }
-
创建异常类
习惯上,定义的类应该包含两个构造器,一个是默认的构造器;另一个是带有详细描述信息的构造器(超类Throwable的 toString方法将会打印出这些详细信息,这在调试中非常有用)。package Test.Exception; import java.io.IOException; public class FileFormatException extends IOException { /** * */ public FileFormatException() { } /** * @param message */ public FileFormatException(String message) { super(message); } }
2. 捕获异常
-
捕获异常
要想捕获一个异常,必须设置try/catch语句块。最简单的try语句块如下所示:try { code more code more code }catch(Exception e) { handler for this type }
请记住,编译器严格地执行throws 说明符。如果调用了一个抛出受查异常的方法,就必须对它进行处理,或者继续传递。
通常,应该捕获那些知道如何处理的异常,而将那些不知道怎样处理的异常继续进行传递。
同时请记住,这个规则也有一个例外。前面曾经提到过:如果编写一个覆盖超类的方法,而这个方法又没有抛出异常(如JComponent中的paintComponent),那么这个方法就必须捕获方法代码中出现的每一个受查异常。不允许在子类的 throws说明符中出现超过超类方法所列出的异常类范围。 -
捕获多个异常
try { code that throws Exception }catch(FileNotFoundException) { }catch(....){ }catch(...) { }
-
再次抛出异常与异常链
原始异常设置为新异常的“原因”:try { ... }catch(SQLException e) { Throwable se = new ServletException("database error"); se.initCause(); throw se; }
-
finally子句
不管是否有异常被捕获,finally子句中的代码都被执行。提示:
强烈建议解耦合try/catch和 try/finally 语句块。这样可以提高代码的清晰度。例如:
try {
try { ... }finally { } }catch(...) { ... }
-
带资源的try语句
带资源的try 语句( try-with-resources)的最简形式为:try(Resource res = ...) { ... }
try块退出时,会自动调用res.close()。
try (Scanner in = new Scanner(new FileInputStream(" /usr/share/dict/words"),"UTF-8"); Printwriter out = new Printwriter("out.txt")) { while (in.hasNext()) out.print1n(in.next(.toUpperCaseO); }
-
分析堆栈轨迹元素
堆栈轨迹( stack trace)是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。
可以调用Throwable类的 printStackTrace方法访问堆栈轨迹的文本描述信息。
一种更灵活的方法是使用getStackTrace方法,它会得到StackTraceElement对象的一个数组,可以在你的程序中分析这个对象数组。
StackTraceElement类含有能够获得文件名和当前执行的代码行号的方法,同时,还含有能够获得类名和方法名的方法。toString方法将产生一个格式化的字符串,其中包含所获得的信息。
静态的Thread.getAllStackTrace方法,它可以产生所有线程的堆栈轨迹。下面给出使用这个方法的具体方式:package Test.Exception; import java.util.Scanner; public class StackTraceTest { public static int factorial(int n) { System.out.print("factorial(" + n + "):"); Throwable throwable = new Throwable(); StackTraceElement[] frames = throwable.getStackTrace(); for (StackTraceElement stackTraceElement : frames) { System.out.println(stackTraceElement); } int r; if (n <= 1) r = 1; else r = n * factorial(n - 1); System.out.println("return " + r); return r; } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("Enter n:"); int n = scanner.nextInt(); factorial(n); scanner.close(); } }
3. 使用异常机制的技巧
- 异常处理不能替代简单的测试:只在异常情况下使用异常机制;
- 不要过分的细化异常
- 利用异常层次结构
- 不要只抛出RuntimeException异常。应该寻找更加适当的子类或创建自己的异常类。
- 不要只捕获Thowable异常,否则,会使程序代码更难读、更难维护。
- 考虑受查异常与非受查异常的区别。
- 将一种异常转换成另一种更加适合的异常时不要犹豫。
- 不要压制异常
- 将一种异常转换成另一种更加适合的异常时不要犹豫。
- 不要羞于传递异常
4. 使用断言
-
断言的概念
断言机制允许在测试期间向代码中插人一些检查语句。当代码发布时,这些插入的检测语句将会被自动地移走。Java语言引入了关键字assert。这个关键字有两种形式:
assert 条件
和
assert 条件:表达式
这两种形式都会对条件进行检测,如果结果为false,则抛出一个AssertionError异常。在第二种形式中,表达式将被传入AssertionError 的构造器,并转换成一个消息字符串。注释:“表达式”部分的唯一目的是产生一个消息字符串。AssertionError对象并不存储表达式的值,因此,不可能在以后得到它。
C++注释:C语言中的assert 宏将断言中的条件转换成一个字符串。当断言失败时,这个字符串将会被打印出来。例如,若 assert(x>=0)失败,那么将打印出失败条件“x>=0”。在Java中,条件并不会自动地成为错误报告中的一部分。如果希望看到这个条件,就必须将它以字符串的形式传递给AssertionError对象: assert x >=0 : "x >=0”。 -
启用和禁用断言
在默认情况下,断言被禁用。可以在运行程序时用-enableassertions或-ea选项启用:
java -enableassertions MyApp需要注意的是,在启用或禁用断言时不必重新编译程序。启用或禁用断言是类加载器( class loader)的功能。当断言被禁用时,类加载器将跳过断言代码,因此,不会降低程序运行的速度。
-
使用断言完成参数检查
在Java语言中,给出了3种处理系统错误的机制:- 抛出一个异常
- 日志
- 使用断言
什么时候应该选择使用断言呢?请记住下面几点:
- 断言失败是致命的、不可恢复的错误。
- 断言检查只用于开发和测阶段(这种做法有时候被戏称为“在靠近海岸时穿上救生衣,但在海中央时就把救生衣抛掉吧”)。
因此,不应该使用断言向程序的其他部分通告发生了可恢复性的错误,或者,不应该作为程序向用户通告问题的手段。断言只应该用于在测试阶段确定程序内部的错误位置。
- 为文档假设使用断言
更好的做法:if(i % 3 == 0) ... else if(i % 3 == 1) ... else // i % 3 ==2 ...
if(i % 3 == 0) ... else if(i % 3 == 1) ... else { assert i % 3 == 2; ... } ...
5. 记录日志
记录日志API的优点:
- 可以很容易地取消全部日志记录,或者仅仅取消某个级别的日志,而且打开和关闭这个操作也很容易。
- 可以很简单地禁止日志记录的输出,因此,将这些日志代码留在程序中的开销很小。
- 日志记录可以被定向到不同的处理器,用于在控制台中显示,用于存储在文件中等。
- 日志记录器和处理器都可以对记录进行过滤。过滤器可以根据过滤实现器制定的标准丢弃那些无用的记录项。
- 日志记录可以采用不同的方式格式化,例如,纯文本或XML。
- 应用程序可以使用多个日志记录器,它们使用类似包名的这种具有层次结构的名字,例如,com.mycompany.myapp。
- 在默认情况下,日志系统的配置由配置文件控制。如果需要的话,应用程序可以替换这个配置。
-
基本日志
要生成简单的日志记录,可以使用全局日志记录器(global logger)并调用其info方法:
Logger.GetGlobal().info(“File->Open menu item selectd”);
打印内容:
May 10,201310:12:15 PM LoggingImageViewer fileOpenINFO: File->0pen menu item selected
但是,如果在适当的地方(如 main开始)调用
Logger.getGlobal().setLeve1(Level.OFF);
将会取消所有的日志。 -
高级日志
可以调用getLogger 方法创建或获取记录器:
private static final Logger myLogger = Logger.getLogger(“com.mycompany.myapp”);
与包名类似,日志记录器名也具有层次结构。事实上,与包名相比,日志记录器的层次性更强。对于包来说,一个包的名字与其父包的名字之间没有语义关系,但是日志记录器的父与子之间将共享某些属性。例如,如果对com.mycompany日志记录器设置了日志级别,它的子记录器也会继承这个级别。
通常,有以下7个日志记录器级别:- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
在默认情况下,只记录前三个级别。也可以设置其他的级别。例如:
logger.setLevel(Level.FINE);
另外,还可以使用Level.ALL开启所有级别的记录,或者使用Level.OFF关闭所有级别的记录。
对于所有的级别有下面几种记录方法:
logger.warning(message);
logger.fine(message);
同时,还可以使用log方法指定级别,例如:
logger.log(Level.FINE,message);
-
修改日志管理器配置
默认情况下,配置文件存在于:
jre/lib/logging.properties
要想使用另一个配置文件,就要将java.util.logging.config.file特性设置为配置文件的存储位置,并用下列命令启动应用程序:
java -Djava.util.logging.config.file = configFile MainClass
要想修改默认的日志记录级别,就需要编辑配置文件,并修改以下命令行
.level = INFO
可以通过添加以下内容来指定自己的日志记录级别
com.application.myapp.level = FINE -
本地化
本地化的应用程序包含资源包( resource bundle)中的本地特定信息。资源包由各个地E(如美国或德国)的映射集合组成。
一个程序可以包含多个资源包,一个用于菜单;其他用于日志消息。每个资源包都有一个名字(如com.mycompany.logmessages)。要想将映射添加到一个资源包中,需要为每个地区创建一个文件。 -
处理器
在默认情况下,日志记录器将记录发送到ConsoleHandler 中,并由它输出到System.err流中。特别是,日志记录器还会将记录发送到父处理器中,而最终的处理器(命名为“”")有一个ConsoleHandler。
要想记录FINE级别的日志,就必须修改配置文件中的默认日志记录级别和处理器级别。另外,还可以绕过配置文件,安装自己的处理器。Logger logger = Logger.getLogger("com.mycompany.myapp"); logger.setLevel(level.FINE); logger.setUseParentHandler(false); Handler handler = new ConsoleHandler(); handler.setLl(level.FINE); logger.addHandler(handler);
要想将日志记录发送到其他地方,就要添加其他的处理器。日志API为此提供了两个很有用的处理器,一个是FileHandler;另一个是SocketHandler。SocketHandler将记录发送到特定的主机和端口。而更令人感兴趣的是FileHandler,它可以收集文件中的记录。
可以像下面这样直接将记录发送到默认文件的处理器:
FileHandler handler = new FileHandler();
logger.addHandler(hand1er);
可以通过设置日志管理器配置文件中的不同参数(请参看表),或者利用其他的构造器(请参看API注释)来修改文件处理器的默认行为。文件处理器配置参数
配置属性 描述 默认值 java.util.logging.FileHandeler.level 处理器级别 默认值 java.util.logging.FileHandler.append 控制处理器应该追加到一个已经存在的文件尾部;
还是应该为每个运行的程序打开一个新文件false java.util.logging.FileHandler.limit 在打开另一个文件之前允许写人一个文件的
近似最大字节数(О表示无限制)在FileHandler类中为0(表示无限制);
在默认的日志管理器配置文件中为50000java.util.logging.FileHandler.pattern 日志文件名的模式,详情看下表 %h/java%u.log java.util.logging.FileHandler.count 在循环列表中的日志记录数 1(不循环) java.util.logging.FileHandler.filter 使用过的顾虑器 没有使用过滤器 java.util.logging.FileHandler.encoding 使用的字符编码 平台的编码 java.util.logging.FileHandler.formatter 记录格式 java.util.logging.XMLFormatter 日志记录文件模式变量
变量 描述 %h 系统属性user.home的值 %t 系统临时目录 %u 用于解决冲突的唯一编号 %g 为循环日志记录生成的数值。(当使用循环功能且模式不包括%g 时,使用后缀%g) %% %字符 -
过滤器
在默认情况下,过滤器根据日志记录的级别进行过滤。每个日志记录器和处理器都可以有一个可选的过滤器来完成附加的过滤。另外,可以通过实现 Filter接口并定义下列方法来自定义过滤器。
boolean isLoggable(LogRecord record)
要想将一个过滤器安装到一个日志记录器或处理器中,只需要调用setFilter方法就可以了。注意,同一时刻最多只能有一个过滤器。 -
格式化器
ConsoleHandler类和 FileHandler类可以生成文本和XML格式的日志记录。但是,也可以自定义格式。这需要扩展Formatter类并覆盖下面这个方法:
String format(LogRecord record)
可以根据自己的愿望对记录中的信息进行格式化,并返回结果字符串。在 format方法中,有可能会调用下面这个方法
String formatMessage(LogRecord record)
这个方法对记录中的部分消息进行格式化、参数替换和本地化应用操作。 -
日志记录说明
-
为一个简单的应用程序,选择一个日志记录器,并把日志记录器命名为与主应用程序包一样的名字,例如,com.mycompany.myprog,这是一种好的编程习惯。
为了方便起见,可能希望利用一些日志操作将下面的静态域添加到类中:
private static final Logger logger = Logger.getLogger(“com.mycompany.myprog”"); -
默认的日志配置将级别等于或高于INFO级别的所有消息记录到控制台。用户可以覆盖默认的配置文件。但是正如前面所述,改变配置需要做相当多的工作。因此,最好在应用程序中安装一个更加适宜的默认配置。
下列代码确保将所有的消息记录到应用程序特定的文件中。可以将这段代码放置在应用程序的 main方法中。
if (System.getProperty("java.uti1.1ogging.config.class"") == nul1 && System.getProperty("java.utii.logging.config.fi1e") == nu11) { try{ Logger.getLogger("").setLeve1(Leve1.ALL);final int LOG_ROTATION_COUNT = 10; Handler handler = new FileHandler("%h/myapp.log",0,LOG_ROTATION_COUNT); Logger.getLogger("").addHandler(handler); }catch (IOException e) { logger.log(Leve1.SEVERE,"Can't create log file handler",e); }
- 现在,可以记录自己想要的内容了。但需要牢记:所有级别为INFO、WARNING和SEVERE的消息都将显示到控制台上。因此,最好只将对程序用户有意义的消息设置为这几个级别。将程序员想要的日志记录,设定为FINE是一个很好的选择。
-
-
示例:
package Test.logging; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.util.logging.FileHandler; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.logging.StreamHandler; import javax.swing.ImageIcon; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JMenuItem; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.filechooser.FileFilter; /** * A modification of the image viewer program that logs various events */ public class LoggingTest { public static void main(String[] args) { if (System.getProperty("java.util.logging.config.class") == null && System.getProperty("java.util.logging.config.file") == null) { try { Logger.getLogger("com.horstmann.corejava").setLevel(Level.ALL); final int LOG_ROTATION_COUNT = 10; Handler handler = new FileHandler("./logs/LoggingTest.log", 0, LOG_ROTATION_COUNT); Logger.getLogger("com.horstmann.corejava").addHandler(handler); } catch (IOException e) { Logger.getLogger("com.horstmann.corejava").log(Level.SEVERE, "Can't create log file handler", e); } } EventQueue.invokeLater(() -> { Handler windowHandler = new WindowHandler(); windowHandler.setLevel(Level.ALL); Logger.getLogger("com.horstmann.corejava").addHandler(windowHandler); JFrame frame = new ImageViewFrame(); frame.setTitle("LoggingTest"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Logger.getLogger("com.horstmann.corejava").fine("Showing frame"); frame.setVisible(true); }); } } class ImageViewFrame extends JFrame { private static final int DEFAULT_WIDTH = 300; private static final int DEFAULT_HEIGHT = 400; private JLabel label; private static Logger logger = Logger.getLogger("com.horstmann.corejava"); public ImageViewFrame() { logger.entering("LoggingTest", "<init>"); setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); // set up menu bar JMenuBar menuBar = new JMenuBar(); setJMenuBar(menuBar); JMenu menu = new JMenu("File"); menuBar.add(menu); JMenuItem openItem = new JMenuItem("Open"); menu.add(openItem); openItem.addActionListener(new FileOpenListener()); JMenuItem exitItem = new JMenuItem("exit"); menu.add(exitItem); exitItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { logger.fine("Exitting"); System.exit(0); } }); label = new JLabel(); add(label); logger.exiting("ImageViewFrame", "<init>"); } private class FileOpenListener implements ActionListener { @Override public void actionPerformed(ActionEvent e) { logger.entering("ImageViewFrame.FileOpenListener", "actionPerformed", e); // set file chooser JFileChooser chooser = new JFileChooser(); chooser.setFileFilter(new FileFilter() { @Override public boolean accept(File f) { return f.getName().toLowerCase().endsWith(".jpg") || f.isDirectory(); } @Override public String getDescription() { return "GIF Images"; } }); // show file chooser dialog int res = chooser.showOpenDialog(ImageViewFrame.this); // if image file accepted,set it as icon of the label if (res == JFileChooser.APPROVE_OPTION) { String name = chooser.getSelectedFile().getPath(); logger.log(Level.FINE, "Reading file {0}", name); label.setIcon(new ImageIcon(name)); } else { logger.fine("File open dialog canceled."); logger.exiting("ImageViewFrame.FileOpenListener", "actionPerformed"); } } } } class WindowHandler extends StreamHandler { private JFrame frame; public WindowHandler() { frame = new JFrame(); final JTextArea output = new JTextArea(); output.setEditable(false); frame.setSize(200, 200); frame.add(new JScrollPane(output)); frame.setFocusableWindowState(false); frame.setVisible(true); setOutputStream(new OutputStream() { @Override public void write(int b) throws IOException { // not called } public void write(byte[] b, int off, int len) { output.append(new String(b, off, len)); } }); } public void publish(LogRecord record) { if(!frame.isVisible()) { return; } super.publish(record); flush(); } }
6. 调试技巧
1. 打印变量值
2. 每一个类中防止一个main方法
3. 使用Junit
4. 日志代理
5. 利用Throwable类提供的printStackTrace方法打印堆栈信息
6. 将错误信息保存在文件中
7. 尽量不要让捕获异常的堆栈轨迹出现在System.err中
8. 要想观察类的加载过程,可以用-verbose标志启动Java虚拟机。
9. -Xlint选项告诉编译器对一些普遍容易出现的代码问题进行检查。
10. Java虚拟机增加了对Java应用程序进行监控(monitoring)和管理(management)的支持。它允许利用虚拟机中的代理装置跟踪内存消耗、线程使用、类加载等情况。这个功能对于像应用程序服务器这样大型的、长时间运行的Java程序来说特别重要。
- 找出运行虚拟机的操作系统进程的ID。
- 在Windows环境下,使用任务管理器。然后运行jconsole程序:
jconsole processID
八、 泛型程序设计
1. 为什么要泛型程序设计
泛型程序设计(Generic Programming)
意味着编写的代码可以被很多不同类型的对象所重用。
-
类型参数的好处
在Java中增加范型类之前,泛型程序设计是用继承实现的。
ArrayList只维护一个Object类型的数组:
public class ArrayList {
private Object[] elements;
…
}
这种方法有两个问题:- 当获取一个值是必须进行强制类型转换。
- 没有错误检查。
类型参数可以很好地解决上述问题。
类型参数的魅力在于:使得程序有更好的可读性和安全性。 -
谁想成为泛型程序员
泛型程序设计划分为3个能力级别:- 基本级别:仅仅使用泛型类,不必考虑他们的工作方式与原因。
2. 定义简单泛型类
package Test.GenericProgramming;
public class Pair<T> {
private T first;
private T second;
/**
*
*/
public Pair() {
first = null;
second = null;
}
/**
* @param first
* @param second
*/
public Pair(T first, T second) {
this.first = first;
this.second = second;
}
/**
* @return the first
*/
public T getFirst() {
return first;
}
/**
* @param first the first to set
*/
public void setFirst(T first) {
this.first = first;
}
/**
* @return the second
*/
public T getSecond() {
return second;
}
/**
* @param second the second to set
*/
public void setSecond(T second) {
this.second = second;
}
}
注释:类型变量使用大写形式,且比较短,这是很常见的。在Java库中,使用变量E表示集合的元素类型,K和V分别表示表的关键字与值的类型。T(需要时还可以用临近的字母U和S)表示“任意类型”。
3. 泛型方法
class ArrayAlg {
public static <T> T getMiddle(T...a) {
return a[a.length / 2];
}
}
这个方法是在普通类中定义的,而不是在泛型类中定义的。然而,这是一个泛型方法,可以从尖括号和类型变量看出这一点。注意,类型变量放在修饰符(这里是public static)的后面,返回类型的前面。
泛型方法可以定义在普通类中,也可以定义在泛型类中。
当调用一个泛型方法时,在方法名前的尖括号中放入具体的类型:
String middle = ArrayAlg.getMiddle(“John”,“Q.”,“Public”);
4. 类型变量的限定
如何确定参数类型T实现了某一个接口(Comparable)或者继承了某一个类?
解决这个问题的方案是将T限制为实现了Comparable接口(只含一个方法 compareTo 的标准接口)的类。可以通过对类型变量T设置限定(bound)实现这一点:
public static T min(T…t){…}
下面的记法:
表示T应该是绑定类型的子类型(subtype)。T和绑定类型可以是类,也可以是接口。选择关键字extends的原因是更接近子类的概念,并且Java的设计者也不打算在语言中再添加一个新的关键字(如 sub)。
一个类型变量或通配符可以有多个限定,例如:
T extends Comparable & Serializable
限定类型用“&”分隔,而逗号用来分隔类型变量。
package Test.GenericProgramming;
import java.time.LocalDate;
public class PairTest {
public static void main(String[] args) {
LocalDate[] birthdays = {
LocalDate.of(2000, 12, 27),
LocalDate.of(1999, 04, 15),
LocalDate.of(1989, 1, 15),
LocalDate.of(2003, 07, 25)
};
Pair<LocalDate> mm = ArrayAlg.minmax(birthdays);
System.out.println("min = "+mm.getFirst());
System.out.println("max = "+mm.getSecond());
}
}
class ArrayAlg {
public static <T extends Comparable> Pair<T> minmax(T[] a) {
if (a == null || a.length == 0)
return null;
T min = a[0];
T max = a[0];
for (T t : a) {
if (min.compareTo(t) > 0)
min = t;
if (max.compareTo(t) < 0)
max = t;
}
return new Pair<>(min, max);
}
}
5. 泛型代码和虚拟机
-
类型擦除
无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type)。原始类型的名字就是删去类型参数后的泛型类型名。擦除( erased)类型变量,并替换为限定类型(无限定的变量用Object)。
原始类型用第一个限定的类型变量来替换,如果没有给定限定就用Object替换。 -
翻译泛型表达式
当程序调用泛型方法时,如果擦除返回类型,编译器插人强制类型转换。 -
翻译泛型方法
程序员通常认为下述的泛型方法:
public static T min(T[] a)
是一个完整的方法族,而擦除类型之后,只剩下一个方法:
public static Comparable min(Comparable[] a)
注意,类型参数T已经被擦除了,只留下了限定类型Comparable。桥方法的概念
总之,需要记住有关Java泛型转换的事实:
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都用它们的限定类型替换。
- 桥方法被合成来保持多态。
- 为保持类型安全性,必要时插入强制类型转换。
-
调用遗留代码
设计Java泛型类型时,主要目标是允许泛型代码和遗留代码之间能够互操作。
6. 约束与局限性
- 不能用基本类型实例化类型参数
- 运行时类型查询只适用于原始类型
- 不能创建参数化类型的数组
- Varargs警告
- 不能实例化类型变量
- 不能构造泛型数组
- 泛型类的静态上下文中类型变量无效
- 不能抛出或捕获泛型类的实例
- 可以消除对受查异常的检查
- 注意擦除后的冲突
7. 泛型类型的继承规则
无论S与T有什么联系,通常,Pair< S >与Pair< T >没有什么联系。
最后,泛型类可以扩展或实现其他的泛型类。就这一点而言,与普通的类没有什么区别。例如,ArrayList类实现List接口。这意味着,一个ArrayList可以被转换为一个List。但是,如前面所见,一个ArrayList不是一个ArrayList 或List。
8. 通配符类型
-
通配符概念
通配符类型中,允许类型参数变化。例如,通配符类
Pair<? extends Employee>
表示任何泛型Pair类型,它的类型参数是Employee的子类,如Pair,但不是Pair。 -
通配符的超类型限定
通配符限定与类型变量限定十分类似,但是,还有一个附加的能力,即可以指定一个超类型限定( supertype bound),如下所示:
? super Manager
这个通配符限制为Manager 的所有超类型。(已有的super关键字十分准确地描述了这种联系,这一点令人感到非常欣慰。)
下面是一个典型的示例。有一个经理的数组,并且想把奖金最高和最低的经理放在一个Pair对象中。Pair的类型是什么?在这里,Pair是合理的,Pair也是合理的。下面的方法将可以接受任何适当的 Pair :public static void minmaxBonus(Manager[]a,Pair<? super Manager> result){ if (a.length == 0) return; Manager min = a[0]; Manager max = a[0]; for (int i = 1; i< a.length; i++){ if (min.getBonus( > a[i].getBonusO) min = a[i]; if (max.getBonus < a[i].getBonusO) max = a[i]; result.setFirst(min); result.setSecond(max); }
直观地讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。
-
无限定通配符
还可以使用无限定的通配符,例如,Pair<?>。
但是如果有更改器(setter)和获取器(getter),只能使用获取器(getter)。 -
捕获通配符
package Test.GenericProgramming; import java.util.logging.Level; import java.util.logging.Logger; import Test.Employee.Employee; import Test.Employee.Manager; public class PairTest2 { public static void main(String[] args) { Manager ceo = new Manager("zunnajim", 3000, 2020, 12, 1, 10000); Manager cfo = new Manager("lsl", 2000, 2020, 12, 1, 20000); Pair<Manager> buddies = new Pair<>(ceo, cfo); printBuddis(buddies); Manager[] managers = { ceo, cfo }; Pair<Employee> result = new Pair<>(); minmaxBonus(managers, result); System.out.println("first: " + result.getFirst().getName() + ", second: " + result.getSecond().getName()); maxminBonus(managers, result); System.out.println("first: " + result.getFirst().getName() + ", second: " + result.getSecond().getName()); } public static void printBuddis(Pair<? extends Employee> pair) { Employee first = pair.getFirst(); Employee second = pair.getSecond(); System.out.println(first.getName() + " and " + second.getName() + " are buddies."); } public static void minmaxBonus(Manager[] managers, Pair<? super Manager> result) { if (managers == null || managers.length == 0) return; Manager min = managers[0]; Manager max = managers[0]; for (Manager manager : managers) { if (min.getBonus() > manager.getBonus()) min = manager; if (max.getBonus() < manager.getBonus()) max = manager; } result.setFirst(min); result.setSecond(max); } public static void maxminBonus(Manager[] managers, Pair<? super Manager> result) { minmaxBonus(managers, result); PairAlg.swap(result); } } class PairAlg { public static boolean hasNull(Pair<?> pair) { return pair.getFirst() == null || pair.getSecond() == null; } public static void swap(Pair<?> pair) { if(hasNull(pair)){ Logger.getLogger("com.hostmann.javacore").log(Level.SEVERE, "One Of The Params Are NULL!"); System.exit(0); } swapHelper(pair); } private static <T> void swapHelper(Pair<T> pair) { T temp = pair.getFirst(); pair.setFirst(pair.getSecond()); pair.setSecond(temp); } }
9. 反射和泛型
-
泛型Class类
现在,Class类是泛型的。例如,String.class实际上是一个Class类的对象(事实上,是唯一的对象)。
类型参数十分有用,这是因为它允许Class方法的返回类型更加具有针对性。下面Class中的方法就使用了类型参数:
T newInstance()
T cast(Object obj)
T[] getEnumConstants()
Class<? super T> getSuperclass()
Constructor getConstructor(Class… parameterTypes)
Constructor getDeclaredConstructor(Class… parameterTypes)
newInstance方法返回一个实例,这个实例所属的类由默认的构造器获得。它的返回类型目前被声明为T,其类型与Class描述的类相同,这样就免除了类型转换。
如果给定的类型确实是T的一个子类型,cast方法就会返回一个现在声明为类型T的对象,否则,抛出一个BadCastException异常。
如果这个类不是enum类或类型T的枚举值的数组,getEnumConstants方法将返回null。最后,getConstructor 与 getdeclaredConstructor方法返回一个Constructor对象。Constructor类也已经变成泛型,以便newInstance方法有一个正确的返回类型。 -
使用Class参数进行类型匹配
有时,匹配泛型方法中的Class参数的类型变量很有实用价值。下面是一个标准的示例:
public static Pair makePair(Class c) throws InstantiationException,IllegalAccessException
{
return new Pair>(c.newInstance(,c.newInstance();
}
如果调用
makePair(Employee.class)
Employee.class是类型Class的一个对象。makePair方法的类型参数T同Employee匹配,并且编译器可以推断出这个方法将返回一个Pair。 -
虚拟机中的泛型类型信息
Java泛型的卓越特性之一是在虚拟机中泛型类型的擦除。令人感到奇怪的是,擦除的类仍然保留一些泛型祖先的微弱记忆。例如,原始的Pair类知道源于泛型类Pair,即使一个Pair类型的对象无法区分是由Pair构造的还是由Pair构造的。类似地,看一下方法
public static Comparable min(Comparable[]a)
这是一个泛型方法的擦除
public static <T extends Comparable<? super T>> T min(T[] a)
可以使用反射API来确定:- 这个泛型方法有一个叫做T的类型参数。
- 这个类型参数有一个子类型限定,其自身又是一个泛型类型。
- 这个限定类型有一个通配符参数。
- 这个通配符参数有一个超类型限定。
- 这个泛型方法有一个泛型数组参数。
换句话说,需要重新构造实现者声明的泛型类以及方法中的所有内容。但是,不会知道对于特定的对象或方法调用,如何解释类型参数。
为了表达泛型类型声明,使用java.lang.reflect包中提供的接口Type。这个接口包含下列子类型: - Class类,描述具体类型。
- TypeVariable接口,描述类型变量(如T extends Comparable<? super T>)。
- WildcardType接口,描述通配符(如? super T)。
- ParameterizedType接口,描述泛型类或接口类型(如 Comparable<? super T>)。
- GenericArrayType接口,描述泛型数组(如 T[ ])。
九、 集合
1. Java集合框架
-
将集合的接口与实现分离
队列接口指出可以在队列的尾部添加元素,在队列的头部删除元素,并且可以查找队列中元素的个数。当需要收集对象,并按照“先进先出”的规则检索对象时就应该使用队列。
当在程序中使用队列时,一旦构建了集合就不需要知道究竟使用了哪种实现。因此,只有在构建集合对象时,使用具体的类才有意义。 -
Collection
接口
在 Java类库中,集合类的基本接口是Collection接口。 -
迭代器
Iterator接口包含四个方法:public interface Iterator<E> { E next(); boolean hasNext(); void remove(); defalut void forEachRemaining(Consume<? super E> action); }
通过反复调用next方法,可以逐个访问集合中的每个元素。但是,如果到达了集合的末尾,next方法将抛出一个NoSuchElementException。因此,需要在调用next之前调用hasNext方法。如果迭代器对象还有多个供访问的元素,这个方法就返回true。如果想要查看集合中的所有元素,就请求一个迭代器,并在 hasNext返回true时反复地调用next方法。
“for each”循环可以与任何实现了Iterable接口的对象一起工作,这个接口只包含一个抽象方法:public interface Iterabale<E> { Iterator<E> iterator(); }
Iterator接口的remove方法将会删除上次调用next方法时返回的元素。
更重要的是,对next方法和remove方法的调用具有互相依赖性。如果调用remove之前没有调用next将是不合法的。如果这样做,将会抛出一个IllegalStateException异常。 -
泛型实用方法
由于Collection与Iterator都是泛型接口,可以编写操作任何集合类型的实用方法。
Collection接口声明了很多有用的方法,所有的实现类都必须提供这些方法。
为了能够让实现者更容易地实现这个接口,Java类库提供了一个类AbstractCollection。 -
集合框架中的接口
Java集合框架为不同类型的集合定义了大量接口,集合有两个基本接口:Collection和 Map。
List是一个有序集合( ordered collection)。元素会增加到容器中的特定位置。可以采用两种方式访问元素:使用迭代器访问,或者使用一个整数索引来访问。后一种方法称为随机访问( random access),因为这样可以按任意顺序访问元素。与之不同,使用迭代器访问时,必须顺序地访问元素。
[Set}(https://docs.oracle.com/javase/8/docs/api/java/util/Set.html)接口等同于Collection接口,不过其方法的行为有更严谨的定义。集 (set)的add方法不允许增加重复的元素。要适当地定义集的equals方法:只要两个集包含同样的元素就认为是相等的,而不要求这些元素有同样的顺序。hashCode方法的定义要保证包含相同元素的两个集会得到相同的散列码。
2. 具体的集合
Java中的具体集合
集合类型 | 描述 |
---|---|
ArrayList | 一种可以动态增长和缩减的索引序列(静态数组) |
LinkedList | 一种可以在任何位置进行高效的插入和删除的有序序列 |
AyyayDeque | 一种用循环数组实现的双端队列 |
TreeSet | 一种有序集 |
EnumSet | 一种包含枚举类型的集合 |
LinkedHashSet | 一种可以记住元素插入顺序的集合 |
PriorityQueue | 一种允许高效删除最小元素的集合 |
HashMap | 一种存储键值对的数据结构 |
TreeMap | 一种键值有序排列的映射表 |
EnumMap | 一种键值属于枚举类型的集合 |
LinkedHashMap | 一种可以记住键值添加次序的映射表 |
WeakHashMap | 一种其值无用武之地后可以被垃圾回收器回收的映射表 |
IdentityHashMap | 一种使用==而不是equals比较的映射表 |
-
链表
在Java程序语言中的链表都是双向链表(double linked)。
LinkedList中的add方法永远会将元素添加到链表尾部。
ListIterator中的add方法才可以在任意位置添加元素。
如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状况。例如,一个迭代器指向另一个迭代器刚刚删除的元素前面,现在这个迭代器就是无效的,并且不应该再使用。链表迭代器的设计使它能够检测到这种修改。如果迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的方法修改了,就会抛出一个ConcurrentModificationException
异常。
使用链表的唯一理由是尽可能地减少在列表中间插人或删除元素所付出的代价。如果列表只有少数几个元素,就完全可以使用ArrayList。 -
数组列表ArrayList
提示:ArrayList不是线程安全的,因此在多线程环境下可以使用
[Vector](https://docs.oracle.com/javase/8/docs/api/java/util/Vector.html)
类。但是因为Vector类需要同步方法,因此需要耗费比较多的时间,因此在单线程环境下使用ArrayList可以提升效率。 -
散列集
有一种众所周知的数据结构,可以快速地查找所需要的对象,这就是散列表( hashtable)。散列表为每个对象计算一个整数,称为散列码( hash code)。散列码是由对象的实例域产生的一个整数。更准确地说,具有不同数据域的对象将产生不同的散列码。散了吗们是由String类的hashCode方法产生的。
散列表可以用于实现几个重要的数据结构。其中最简单的是set类型。set是没有重复元素的元素集合。set的 add方法首先在集中查找要添加的对象,如果不存在,就将这个对象添加进去。
Java集合类库提供了一个HashSet类,它实现了基于散列表的集。可以用add方法添加元素。contains方法已经被重新定义,用来快速地查看是否某个元素已经出现在集中。它只在某个桶中查找元素,而不必查看集合中的所有元素。package Test.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Scanner; import java.util.Set; public class SetTest { public static void main(String[] args) { Set<String> words = new HashSet<>(); long totalTime = 0; try (Scanner scanner = new Scanner(System.in)) { while(scanner.hasNext()) { String word = scanner.next(); long callTime = System.currentTimeMillis(); words.add(word); callTime = System.currentTimeMillis() - callTime; totalTime += callTime; } } catch (Exception e) { //TODO: handle exception } Iterator<String> iterator = words.iterator(); for (int i = 0; i < 20 && iterator.hasNext(); i++) { System.out.println(iterator.next()); System.out.println("..."); System.out.println(words.size()+" distinct words. "+totalTime + " millseconds"); } } }
-
树集
TreeSet类与散列集十分类似,不过,它比散列集有所改进。树集是一个有序集合( sorted collection)。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值将自动地按照排序后的顺序呈现。 -
队列与双端队列
队列可以让人们有效地在尾部添加一个元素,在头部删除一个元素。有两个端头的队列,即双端队列,可以让人们有效地在头部和尾部同时添加或删除元素。不支持在队列中间添加元素。 -
优先级队列
优先级队列(priority queue)中的元素可以按照任意的顺序插人,却总是按照排序的顺序进行检索。也就是说,无论何时调用remove方法,总会获得当前优先级队列中最小的元素。然而,优先级队列并没有对所有的元素进行排序。如果用迭代的方式处理这些元素,并不需要对它们进行排序。优先级队列使用了一个优雅且高效的数据结构,称为堆( heap)。堆是一个可以自我调整的二叉树,对树执行添加 ( add)和删除(remore)操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序。
3. 映射
-
Java基本映射操作
Java类库为映射提供了两个通用的实现:HashMap和TreeMap。这两个类都实现了Map接口。散列映射对键进行散列,树映射用键的整体顺序对元素进行排序,并将其组织成搜索树。散列或比较函数只能作用于键。与键关联的值不能进行散列或比较。
键必须是唯一的。不能对同一个键存放两个值。如果对同一个键两次调用put方法,第二个值就会取代第一个值。实际上,put将返回用这个键参数存储的上一个值。 -
更新映射项
- 使用getOrDefault方法,如果键存在,则返回值,不存在则将键插入到映射表中,将值设为默认(提供的第二个参数),并返回默认值。
- 使用putIfAbset方法,只有当键存在时才会放入。
- 使用merge方法(推荐),当键不存在时,插入键并且将值设置为第二个参数。当键存在时,调用第三个函数进行值和第二个参数的操作。
-
映射视图
集合框架不认为映射本身是一个集合。(其他数据结构框架认为映射是一个键/值对集合,或者是由键索引的值集合。)不过,可以得到映射的视图( view)——这是实现了Collection接口或某个子接口的对象。
有3种视图:键集、值集合(不是一个集)以及键/值对集。键和键/值对可以构成一个集,因为映射中一个键只能有一个副本。下面的方法:
Set keySet()
Collection values()
Set<Map.Entry<K,V>> entrySet()
会分别返回这3个视图。 -
弱散列映射
设计WeakHashMap类是为了解决一个有趣的问题。如果有一个值,对应的键已经不再使用了,将会出现什么情况呢?假定对某个键的最后一次引用已经消亡,不再有任何途径引用这个值的对象了。但是,由于在程序中的任何部分没有再出现这个键,所以,这个键/值对无法从映射中删除。为什么垃圾回收器不能够删除它呢?难道删除无用的对象不是垃圾回收器的工作吗?
遗憾的是,事情没有这样简单。垃圾回收器跟踪活动的对象。只要映射对象是活动的,其中的所有桶也是活动的,它们不能被回收。因此,需要由程序负责从长期存活的映射表中删除那些无用的值。或者使用WeakHashMap完成这件事情。当对键的唯一引用来自散列条目时,这一数据结构将与垃圾回收器协同工作一起删除键/值对。
下面是这种机制的内部运行情况:
WeakHashMap使用弱引用( weak references)保存键。WeakReference对象将引用保存到另外一个对象中,在这里,就是散列键。对于这种类型的对象,垃圾回收器用一种特有的方式进行处理。通常,如果垃圾回收器发现某个特定的对象已经没有他人引用了,就将其回收。然而,如果某个对象只能由WeakReference引用,垃圾回收器仍然回收它,但要将引用这个对象的弱引用放入队列中。WeakHashMap将周期性地检查队列,以便找出新添加的弱引用。一个弱引用进入队列意味着这个键不再被他人使用,并且已经被收集起来。于是,WeakHashMap将删除对应的条目。 -
链接散列集与映射
LinkedHashSet和 LinkedHashMap类用来记住插入元素项的顺序。这样就可以避免在散列表中的项从表面上看是随机排列的。当条目插入到表中时,就会并入到双向链表中。
链接散列映射将用访问顺序,而不是插人顺序,对映射条目进行迭代。每次调用get或put,受到影响的条目将从当前的位置删除,并放到条目链表的尾部(只有条目在链表中的位置会受影响,而散列表中的桶不会受影响。一个条目总位于与键散列码对应的桶中)。 -
枚举集与映射
EnumSet是一个枚举类型元素集的高效实现。由于枚举类型只有有限个实例,所以EnumSet内部用位序列实现。如果对应的值在集中,则相应的位被置为1。 -
标识散列映射
类IdentityHashMap有特殊的作用。在这个类中,键的散列值不是用hashCode函数计算的,而是用System.identityHashCode方法计算的。这是Object.hashCode方法根据对象的内存地址来计算散列码时所使用的方式。而且,在对两个对象进行比较时,IdentityHashMap类使用==,而不使用equals。
也就是说,不同的键对象,即使内容相同,也被视为是不同的对象。在实现对象遍历算法(如对象串行化)时,这个类非常有用,可以用来跟踪每个对象的遍历状况。
4. 视图与包装器
-
轻量级集合包装器
Arrays类的静态方法 asList将返回一个包装了普通Java数组的List包装器。这个方法可以将数组传递给一个期望得到列表或集合参数的方法。例如:
Card[] cardDeck = new Card[52];
List cardList = Arrays.asList(cardDeck);
返回的对象不是ArrayList。它是一个视图对象,带有访问底层数组的get和 set方法。改变数组大小的所有方法(例如,与迭代器相关的add和 remove方法)都会抛出一个UnsupportedOperationException异常。
这个方法调用
Collections.nCopies(n,anObject)
将返回一个实现了List接口的不可修改的对象,并给人一种包含n个元素,每个元素都像是一个anObject的错觉。
例如,下面的调用将创建一个包含100个字符串的List,每个串都被设置为"DEFAULT”:
List settings = Collections.nCopies(100,“DEFAULT”);
存储代价很小。这是视图技术的一种巧妙应用。 -
子范围
可以为很多集合建立子范围( subrange)视图。例如,假设有一个列表staff,想从中取出第10个~第19个元素。可以使用subList方法来获得一个列表的子范围视图。
第一个索引包含在内,第二个索引则不包含在内。这与String类的substring操作中的参数情况相同。
可以将任何操作应用于子范围,并且能够自动地反映整个列表的情况。 -
不可修改的视图
Collections还有几个方法,用于产生集合的不可修改视图( unmodifiable views)。这些视图对现有集合增加了一个运行时的检查。如果发现试图对集合进行修改,就抛出一个异常,同时这个集合将保持未修改的状态。
可以使用下面8种方法获得不可修改视图:- Collections.unmodifiableCol1ection
- Collections.unmodifiableList
- Collections.unmodifiableSet
- Co11ections.unmodifiableSortedSet
- Collections.unmodifiableNavigab1eSet
- Collections.unmodifiableMap
- Col1ections.unmodifiableSortedMap
- Co1lections.unmodifiableNavigableMap
-
同步视图
如果由多个线程访问集合,就必须确保集不会被意外地破坏。例如,如果一个线程试图将元素添加到散列表中,同时另一个线程正在对散列表进行再散列,其结果将是灾难性的。
类库的设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合类。例如,Collections类的静态synchronizedMap方法可以将任何一个映射表转换成具有同步访问方法的 Map:
Map<String,Employee> map = Collections.synchronizedMap(new HashMap<String,Employee>0);
现在,就可以由多线程访问map对象了。像get和 put这类方法都是同步操作的,即在另一个线程调用另一个方法之前,刚才的方法调用必须彻底完成。 -
受查视图
“受查”视图用来对泛型类型发生问题时提供调试支持。
5. 算法
-
排序与混排
Collections类中的sort方法可以对实现了List 接口的集合进行排序。
如果想按照降序对列表进行排序,可以使用一种非常方便的静态方法Collections.reverse-Drder)。这个方法将返回一个比较器,比较器则返回b.compareTo(a)。
Collections类有一个算法 shuffle,其功能与排序刚好相反,即随机地混排列表中元素的顺序。 -
二分查找
要想在数组中查找一个对象,通常要依次访问数组中的每个元素,直到找到匹配的元素为止。然而,如果数组是有序的,就可以直接查看位于数组中间的元素,看一看是否大于要查找的元素。如果是,用同样的方法在数组的前半部分继续查找;否则,用同样的方法在数组的后半部分继续查找。这样就可以将查找范围缩减一半。一直用这种方式查找下去。
Collections类的 binarySearch方法实现了这个算法。注意,集合必须是排好序的,否则算法将返回错误的答案。要想查找某个元素,必须提供集合(这个集合要实现List接口)以及要查找的元素。如果集合没有采用Comparable接口的compareTo方法进行排序,就还要提供一个比较器对象。 -
简单算法
在Collections类中包含了几个简单且很有用的算法。前面介绍的查找集合中最大元素的示例就在其中。另外还包括:- 将一个列表中的元素复制到另外一个列表中;
- 用一个常量值填充容器;
- 逆置一个列表的元素顺序。
-
批操作
很多操作会“成批”复制或删除元素。以下调用
co111.removeA11(co112);
将从coll1中删除coll2中出现的所有元素。与之相反
co111.retainA11(co112);
会从coll1中删除所有未在coll2中出现的元素。 -
集合与数组的转换
如果需要把一个数组转换为集合,Arrays.asList包装器可以达到这个目的。
从集合得到数组会更困难一些。当然,可以使用toArray方法。toArray方法返回的数组是一个Object[]数组,不能改变它的类型。
6. 遗留的集合
-
HashTable类
Hashtable类与HashMap类的作用一样,实际上,它们拥有相同的接口。与Vector类的方法一样。Hashtable的方法也是同步的。如果对同步性或与遗留代码的兼容性没有任何要求,就应该使用HashMap。如果需要并发访问,则要使用ConcurrentHashMap。 -
枚举
遗留集合使用Enumeration接口对元素序列进行遍历。Enumeration接口有两个方法,即hasMoreElements和nextElement。这两个方法与Iterator接口的 hasNext方法和next方法十分类似。 -
属性映射
属性映射(property map)是一个类型非常特殊的映射结构。它有下面3个特性:- 键与值都是字符串。
- 表可以保存到一个文件中,也可以从文件中加载。
- 使用一个默认的辅助表。
实现属性映射的Java平台类称为Properties。
-
栈Stack
-
位集
Java平台的BitSet类用于存放一个位序列(它不是数学上的集,称为位向量或位数组更为合适)。如果需要高效地存储位序列(例如,标志)就可以使用位集。由于位集将位包装在字节里,所以,使用位集要比使用Boolean对象的ArrayList更加高效。
BitSet类提供了一个便于读取、设置或清除各个位的接口。使用这个接口可以避免屏蔽和其他麻烦的位操作。如果将这些位存储在int或long变量中就必须进行这些繁琐的操作。
十、图形程序设计
1. Swing概述
由于下列几点无法抗拒的原因,人们选择Swing:
- Swing拥有一个丰富、便捷的用户界面元素集合。
- Swing 对底层平台依赖的很少,因此与平台相关的 bug很少。
- Swing给予不同平台的用户一致的感觉。
2. 创建框架
在Java中,顶层窗口(就是没有包含在其他窗口中的窗口)被称为框架(frame)。在AWT库中有一个称为Frame的类,用于描述顶层窗口。这个类的Swing版本名为JFrame,它扩展于Frame类。JFrame是极少数几个不绘制在画布上的Swing组件之一。因此,它的修饰部件(按钮、标题栏、图标等)由用户的窗口系统绘制,而不是由Swing绘制。
在每个Swing 程序中,有两个技术问题需要强调。
首先,所有的Swing组件必须由事件分派线程(event dispatch thread)
进行配置,线程将鼠标点击和按键控制转移到用户接口组件。
EventQueue.invokeLater(()-> {
statement
...
});
接下来,定义一个用户关闭这个框架时的响应动作。
frame.setDefalutCloseOperation(JFram.EXIT_ON_CLOSE);
在初始化语句结束后,main方法退出。需要注意,退出main并没有终止程序,终止的只是主线程。事件分派线程保持程序处于激活状态,直到关闭框架或调用System.exit方法终止程序。
3.框架定位
JFrame类本身只包含若干个改变框架外观的方法。当然,通过继承,从JFrame 的各个超类中继承了许多用于处理框架大小和位置的方法。其中最重要的有下面几个:
- setLocation和 setBounds方法用于设置框架的位置。
- setIconImage用于告诉窗口系统在标题栏、任务切换窗口等位置显示哪个图标。
- setTitle用于改变标题栏的文字。
- setResizable利用一个boolean值确定框架的大小是否允许用户改变。
JFrame继承层次
Object | | Component | | Container | | _______________|_______________ | | | | JComponent Window | | | | JPanel Frame | | JFrame
-
框架属性
组件类的很多方法是以获取/设置方法对形式出现的,例如,Frame类的下列方法:
public String getTitle()
public void setTitle(String title)
这样的一个获取/设置方法对被称为一种属性。属性包含属性名和类型。
针对get/set约定有一个例外:对于类型为boolean的属性,获取方法由is开头。 -
确定适合的框架大小
要记住:如果没有明确地指定框架的大小,所有框架的默认值为0×0像素。
对于专业应用程序来说,应该检查屏幕的分辨率,并根据其分辨率编写代码重置框架的大小,如在膝上型电脑的屏幕上,正常显示的窗口在高分辨率屏幕上可能会变成一张邮票的大小。
为了得到屏幕的大小,需要按照下列步骤操作。调用Toolkit类的静态方法getDefault-Toolkit得到一个Toolkit对象(Toolkit类包含很多与本地窗口系统打交道的方法)。然后,调用getScreenSize方法,这个方法以Dimension对象的形式返回屏幕的大小。Dimension对象同时用公有实例变量 width和l height保存着屏幕的宽度和高度。下面是相关的代码:
ToolKit kit = ToolKit.getDefaultToolKit();
Dimension screenSize = kit.getScreenSize();
int screenWidth = screenSize.width;
int screenHeight = screenSize.height;
下面是为了处理框架给予的一些提示:
- 如果框架中只包含标准的组件,如按钮和文本框,那么可以通过调用pack 方法设置框架大小。框架将被设置为刚好能够放置所有组件的大小。在通常情况下,将程序的主框架尺寸设置为最大。可以通过调用下列方法将框架设置为最大。
frame.setExtendedState(Frame.MAXIMIZED_BOTH); - 牢记用户定位应用程序的框架位置、重置框架大小,并且在应用程序再次启动时恢复这些内容是一个不错的想法。
- GraphicsDevice类还允许以全屏模式执行应用。
package Test.Swing;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Image;
import java.awt.Toolkit;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
public class SizedFrameTest {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new SizedFrame();
frame.setTitle("SizedFrame");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
class SizedFrame extends JFrame {
public SizedFrame() {
// get screen dimension
Toolkit kit = Toolkit.getDefaultToolkit();
Dimension screenSize = kit.getScreenSize();
int screenHeight = screenSize.height;
int screenWidth = screenSize.height;
setSize(screenWidth / 2, screenHeight / 2);
setLocationByPlatform(true);
Image img = new ImageIcon("D:\\VSCode\\images\\preview.jpg").getImage();
setIconImage(img);
}
}
4. 在组建中显示信息
Swing程序员最关心的是内容窗格( content pane)。在设计框架的时候,要使用下列代码将所有的组件添加到内容窗格中:
Container contentPane = frame.getContentPane();Component c = …;
contentPane.add©;
package Test.Swing;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JFrame;
public class SizedFrameTest {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
// JFrame frame = new SizedFrame();
JFrame frame = new NotHelloWorldFrame();
frame.setTitle("SizedFrame");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
class NotHelloWorldFrame extends JFrame {
public NotHelloWorldFrame() {
NotHelloWorldComponent nComponent = new NotHelloWorldComponent();
add(nComponent);
setSize(nComponent.getPrefeDimension());
// pack();
}
}
class NotHelloWorldComponent extends JComponent {
public static final int MESSAGE_X = 75;
public static final int MESSAGE_Y = 100;
private static final int DEFALUT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 200;
public void paintComponent(Graphics graphic) {
graphic.drawString("Not A Hello World Program!", MESSAGE_X, MESSAGE_Y);
}
public Dimension getPrefeDimension() {
return new Dimension(DEFALUT_WIDTH, DEFAULT_HEIGHT);
}
}
5. 处理2D图形
要想使用Java 2D库绘制图形,需要获得一个Graphics2D类对象。这个类是Graphics类的子类。自从Java SE 2版本以来,paintComponent方法就会自动地获得一个 Graphics2D类对象,我们只需要进行一次类型转换就可以了。如下所示:
public void paintComponent(Graphics g) {
Craphics2D g2 = (Graphics2D) g;
...
}
package Test.Swing;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JComponent;
import javax.swing.JFrame;
public class DrawTest {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new DrawFrame();
frame.setTitle("DrawFrame");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
class DrawFrame extends JFrame {
public DrawFrame() {
DrawComponent dComponent = new DrawComponent();
add(dComponent);
setSize(dComponent.getPreferredSize());
}
}
class DrawComponent extends JComponent {
private static final int DEFALUT_WIDTH = 400;
private static final int DEFAULT_HEIGHT = 400;
public void paintComponent(Graphics graphic) {
Graphics2D g2 = (Graphics2D) graphic;
double leftX = 100;
double leftY = 100;
double width = 200;
double height = 150;
Rectangle2D rect = new Rectangle2D.Double(leftX, leftY, width, height);
g2.draw(rect);
Ellipse2D ellipse = new Ellipse2D.Double();
ellipse.setFrame(rect);
g2.draw(ellipse);
g2.draw(new Line2D.Double(leftX, leftY, leftX + width, leftY + height));
double centerX = rect.getCenterX();
double centerY = rect.getCenterY();
double radius = 150;
Ellipse2D circle = new Ellipse2D.Double();
circle.setFrameFromCenter(centerX, centerY, centerX + radius, centerY + radius);
g2.draw(circle);
}
public Dimension getPreferredDimension() {
return new Dimension(DEFALUT_WIDTH, DEFAULT_HEIGHT);
}
}
6. 使用颜色
使用Graphics2D类的setPaint方法可以为图形环境上的所有后续的绘制操作选择颜色。例如:
g2.setPaint(Color.RED);
g2.drawString(“warning!”,100,100);
只需要将调用draw替换为调用fill就可以用一种颜色填充一个封闭图形(例如:矩形或椭圆)的内部:
Rectangle2D rect = . . .;
g2.setPaint(Color.RED);
g2.fi11(rect);// fi11s rect with red
要想绘制多种颜色,就需要按照选择颜色、绘制图形、再选择另外一种颜色、再绘制图形的过程实施。
要想设置背景颜色,就需要使用Component类中的setBackground方法。Component类是JComponent类的祖先。
MyComponent p = new MyComponent();
p.setBackground(Color.PINK);
另外,还有一个setForeground方法,它是用来设定在组件上进行绘制时使用的默认颜色。
7. 文本使用特殊字体
要想知道某台特定计算机上允许使用的字体,就需要调用GraphicsEnvironment类中的getAvailableFontFamilyNames方法。这个方法将返回一个字符型数组,其中包含了所有可用的字体名。GraphicsEnvironment类描述了用户系统的图形环境,为了得到这个类的对象,需要调用静态的getLocalGraphicsEnvironment方法。下面这个程序将打印出系统上的所有字体名:
public class ListFronts {
public static void main(String[] args) {
String[] fontNames = GraphicsEnvironment.getLocalGraphicsEnvironmrnt().getAvailableFontFamilyNames();
for(String fontName : fontNames)
System.out.print(fontName + " ");
}
}
要想使用某种字体绘制字符,必须首先利用指定的字体名、字体风格和字体大小来创建一个Font类对象。下面是构造一个Font对象的例子:
Font sansbold14 = new Font(“SansSerif”",Font.BOLD.14);
接下来,将字符串绘制在面板的中央,而不是任意位置。因此,需要知道字符串占据的宽和高的像素数量。这两个值取决于下面三个因素:
- 使用的字体(在前面列举的例子中为sans serif,加粗,14号);
- 字符串(在前面列举的例子中为“Hello,World”);
- 绘制字体的设备(在前面列举的例子中为用户屏幕)。
要想得到屏幕设备字体属性的描述对象,需要调用Graphics2D类中的getFontRenderContext方法。它将返回一个FontRenderContext类对象。可以直接将这个对象传递给Font类的getStringBounds方法:
FontRenderContext context = g2.getFontRenderContext();
Rectangle2D bounds = sansbold14.getStringBounds(message,context);
getStringBounds方法将返回包围字符串的矩形。
package Test.Swing;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.colorchooser.ColorChooserComponentFactory;
public class FontTest {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new FontFrame();
frame.setTitle("DrawFrame");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
class FontFrame extends JFrame {
public FontFrame() {
add(new FontComponent());
pack();
}
}
class FontComponent extends JComponent {
private static final int DEFALUT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 200;
public void paintComponent(Graphics graphic) {
Graphics2D g2 = (Graphics2D) graphic;
String message = "Hello, World!";
Font font = new Font("Serif", Font.BOLD, 36);
g2.setFont(font);
FontRenderContext context = g2.getFontRenderContext();
Rectangle2D bounds = font.getStringBounds(message, context);
double x = (getWidth() - bounds.getWidth()) / 2;
double y = (getHeight() - bounds.getHeight()) / 2;
double ascent = -bounds.getY();
double baseY = y + ascent;
g2.setPaint(Color.RED);
g2.drawString(message, (int) x, (int) baseY);
g2.setPaint(Color.CYAN);
g2.draw(new Line2D.Double(x, baseY, x + bounds.getWidth(), baseY));
Rectangle2D rect = new Rectangle2D.Double(x, y, bounds.getWidth(), bounds.getHeight());
g2.draw(rect);
}
public Dimension getPreferredSize() {
return new Dimension(DEFALUT_WIDTH, DEFAULT_HEIGHT);
}
}
8. 显示图像
package Test.Swing;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Image;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JFrame;
public class ImageTest {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new ImageFrame();
frame.setTitle("DrawFrame");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
class ImageFrame extends JFrame {
public ImageFrame() {
add(new ImageComponent());
pack();
}
}
class ImageComponent extends JComponent {
private static final int DEFALUT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 200;
private Image image;
public ImageComponent() {
image = new ImageIcon("D:\\VSCode\\images\\preview.jpg").getImage();
}
public void paintComponent(Graphics graphic) {
if (image == null)
return;
int imageWidth = image.getWidth(this);
int imageHeight = image.getHeight(this);
graphic.drawImage(image, 0, 0, null);
for (int i = 0; i * imageWidth <= getWidth(); i++) {
for (int j = 0; j * imageHeight <= getHeight(); j++) {
graphic.copyArea(0, 0, imageWidth, imageHeight, i * imageWidth, j * imageHeight);
}
}
}
public Dimension getPreferredSize() {
return new Dimension(DEFALUT_WIDTH, DEFAULT_HEIGHT);
}
}
十一、事件处理
1. 事件处理基础
像Java这样的面向对象语言,都将事件的相关信息封装在一个事件对象(event object)
中。在Java中,所有的事件对象都最终派生于java.util.EventObject类
。当然,每个事件类型还有子类,例如,ActionEvent和 WindowEvent。
不同的事件源可以产生不同类别的事件。例如,按钮可以发送一个ActionEvent对象,而窗口可以发送 WindowEvent对象。
综上所述,下面给出AWT事件处理机制的概要:
- 监听器对象是一个实现了
特定监听器接口(listener interface)
的类的实例。 - 事件源是一个能够注册监听器对象并发送事件对象的对象。
- 当事件发生时,事件源将事件对象传递给所有注册的监听器
- 监听器对象将利用事件对象中的信息决定如何对事件做出响应。
-
实例:处理按钮点击事件
package Test.Swing; import java.awt.Color; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JPanel; public class ButtonTest { public static void main(String[] args) { ButtonFrame buttonFrame = new ButtonFrame(); buttonFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); buttonFrame.setVisible(true); } } class ButtonFrame extends JFrame { private JPanel buttonPanel; private static final int DEFALUT_WIDTH = 300; private static final int DEFAULT_HEIGHT = 200; public ButtonFrame() { setSize(DEFALUT_WIDTH, DEFAULT_HEIGHT); JButton yellowButton = new JButton("Yellow"); JButton blueButton = new JButton("Blue"); JButton redButton = new JButton("Red"); buttonPanel = new JPanel(); buttonPanel.add(yellowButton); buttonPanel.add(blueButton); buttonPanel.add(redButton); ColorAction yellowAction = new ColorAction(Color.YELLOW); ColorAction blueAction = new ColorAction(Color.BLUE); ColorAction redAction = new ColorAction(Color.RED); yellowButton.addActionListener(yellowAction); blueButton.addActionListener(blueAction); redButton.addActionListener(redAction); add(buttonPanel); } private class ColorAction implements ActionListener { private final Color backgroundColor; public ColorAction(Color color) { backgroundColor = color; } @Override public void actionPerformed(ActionEvent e) { buttonPanel.setBackground(backgroundColor); } } }
-
简洁的指定监听器
在上一节中,我们为事件监听器定义了一个类并构造了这个类的3个对象。一个监听器类有多个实例的情况并不多见。更常见的情况是:每个监听器执行一个单独的动作。在这种青况下,没有必要分别建立单独的类。只需要使用一个lambda表达式:
exitButton.addActionListener(event -> System.exit(O));
现在考虑这样一种情况:有多个相互关联的动作,如上一节中的彩色按钮。在这种情况下,可以实现一个辅助方法:
public void makeButton(String name,Color backgroundColor){
Button button = new JButton(name);buttonPanel.add(button);
button.addActionListener(event ->
buttonPanel.setBackground(backgroundColor));}
-
实例:改变观感
在默认情况下,Swing程序使用Metal观感,可以采用两种方式改变观感。第一种方式是在Java安装的子目录jre/lib下有一个文件swing.properties。在这个文件中,将属性swing.defaultlaf设置为所希望的观感类名。
第二种方式是动态地改变观感。这需要调用静态的UIManager.setLookAndFeel方法,并提供所想要的观感类名,然后再调用静态方法SwingUtilities.updateComponentTreeUI来刷新全部的组件集。这里需要向这个方法提供一个组件,并由此找到其他的所有组件。 -
适配器类
并不是所有的事件处理都像按钮点击那样简单。在正规的程序中,往往希望用户在确认没有丢失所做工作之后再关闭程序。当用户关闭框架时,可能希望弹出一个对话框来警告用户没有保存的工作有可能会丢失,只有在用户确认之后才退出程序。
当程序用户试图关闭一个框架窗口时,JFrame对象就是WindowEvent的事件源。如果希望捕获这个事件,就必须有一个合适的监听器对象,并将它添加到框架的窗口监听器列表中。
WindowListener 1istener =…;
frame.addwindowListener(listener);
窗口监听器必须是实现 WindowListener接口的类的一个对象。在 WindowListener接口中包含7个方法。当发生窗口事件时,框架将调用这些方法响应7个不同的事件。从它们的名字就可以得知其作用,唯一的例外是在 Windows下,通常将iconified(图标化)称为minimized(最小化)。下面是完整的 WindowListener接口:public interface WindowListener extends EventListener { void windowActivated(WindowEvent e) //Invoked when the Window is set to be the active Window. void windowClosed(WindowEvent e) //Invoked when a window has been closed as the result of calling dispose on the window. void windowClosing(WindowEvent e) //Invoked when the user attempts to close the window from the window's system menu. void windowDeactivated(WindowEvent e) //Invoked when a Window is no longer the active Window. void windowDeiconified(WindowEvent e) //Invoked when a window is changed from a minimized to a normal state. void windowIconified(WindowEvent e) //Invoked when a window is changed from a normal to a minimized state. void windowOpened(WindowEvent e) //Invoked the first time a window is made visible. }
2. 动作
Swing 包提供了一种非常实用的机制来封装命令,并将它们连接到多个事件源,这就是Action接口。一个动作是一个封装下列内容的对象:
- 命令的说明(一个文本字符串和一个可选图标);
- 执行命令所需要的参数(例如,在列举的例子中请求改变的颜色)。
Action接口提供了以下方法:
public interface Action extends ActionListener {
void actionPerformed(ActionEvent e) //Invoked when an action occurs.
void addPropertyChangeListener(PropertyChangeListener listener) //Adds a PropertyChange listener.
Object getValue(String key) //Gets one of this object's properties using the associated key.
boolean isEnabled() //Returns the enabled state of the Action.
void putValue(String key, Object value) //Sets one of this object's properties using the associated key.
void removePropertyChangeListener(PropertyChangeListener listener) //Removes a PropertyChange listener.
void setEnabled(boolean b) //Sets the enabled state of the Action.
}
预定义动作表名称
名称 | 值 |
---|---|
NAME | 动作名称,显示在按钮和菜单上 |
SMALL_ICON | 存储小图标的地方,显示在按钮、菜单项或工具栏中 |
SHORT_DESCRIPTION | 图标的简要说明,显示在工具提示中 |
LONG_DESCRIPTION | 图标的详细说明,显示在在线帮助中 |
MNEMONIC_KEY | 快捷键缩写 |
ACCELERATOR_KEY | 存储加速击键的地方(Swing不使用) |
ACTION_COMMAND_KEY | 历史遗留 |
DEFAULT | 可能有用的综合属性 |
想要将这个动作对象添加到击键中,以便让用户敲击键盘命令来执行这项动作。为了将动作与击键关联起来,首先需要生成KeyStroke类对象。这是一个很有用的类,它封装了对键的说明。要想生成一个KeyStroke对象,不要调用构造器,而是调用KeyStroke类中的静态getKeyStroke方法:
KeyStroke ctrlBKey = KyStroke.getKeyStroke(“ctrl B”);
为了能够理解下一个步骤,需要知道keyboard focus 的概念。用户界面中可以包含许多按钮、菜单、滚动栏以及其他的组件。当用户敲击键盘时,这个动作会被发送给拥有焦点的组件。通常具有焦点的组件可以明显地察觉到(但并不总是这样),例如,在Java观感中,具有焦点的按钮在按钮文本周围有一个细的矩形边框。用户可以使用TAB键在组件之间移动焦点。当按下SPACE键时,就点击了拥有焦点的按钮。还有一些键执行一些其他的动作,例如,按下箭头键可以移动滚动条。
然而,在这里的示例中,并不希望将击键发送给拥有焦点的组件。否则,每个按钮都需要知道如何处理CTRL+Y、CTRL+B和CTRL+R这些组合键。
这是一个常见的问题,Swing 设计者给出了一种很便捷的解决方案。每个JComponent有三个输入映射(imput map),每一个映射的KeyStroke对象都与动作关联。三个输入映射对应着三个不同的条件。
输入映射条件
标志 | 激活动作 |
---|---|
WHEN_FOCUSED | 当这个组件拥有键盘焦点时 |
WHEN_ANCESSTOR_OF_FOCUSED_COMPONENT | 当这个组件包含了拥有键盘焦点的组件时 |
WHEN_IN_FOCUSED_WINDOW | 当这个组件被包含在一个拥有键盘焦点的窗口中时 |
按键处理将按照下列顺序检查这些映射:
- 检查具有输入焦点组件的WHEN_FOCUSED映射。如果这个按键存在,将执行对应的动作。如果动作已启用,则停止处理。
- 从具有输入焦点的组件开始,检查其父组件的WHEN_ANCESTOR_OF_FOCUSEDCOMPONENT 映射。一旦找到按键对应的映射,就执行对应的动作。如果动作已启用,将停止处理。
- 查看具有输入焦点的窗口中的所有可视的和启用的组件,这个按键被注册到WHEN_IN_FOCUSED_WINDOW映射中。给这些组件(按照按键注册的顺序)一个执行对应动作的机会。一旦第一个启用的动作被执行,就停止处理。如果一个按键在多个WHEN_IN_FOCUSED_WINDOW映射中出现,这部分处理就可能会出现问题。
可以使用getInputMap方法从组件中得到输入映射。例如:
InputMap imap = panel.getInputMap(JComponnet.WHEN_FOCUSED);
下面总结一下用同一个动作响应按钮、菜单项或按键的方式: - 实现一个扩展于AbstractAction类的类。多个相关的动作可以使用同一个类。
- 构造一个动作类的对象。
- 使用动作对象创建按钮或菜单项。构造器将从动作对象中读取标签文本和图标。
- 为了能够通过按键触发动作,必须额外地执行几步操作。首先定位顶层窗口组件,例如,包含所有其他组件的面板。
- 然后,得到顶层组件的WHEN_ANCESTOR_OF_FOCUS_COMPONENT输入映射。为需要的按键创建一个KeyStrike对象。创建一个描述动作字符串这样的动作键对象。将(按键,动作键)对添加到输入映射中。
- 最后,得到顶层组件的动作映射。将(动作键,动作对象)添加到映射中。
package Test.Swing;
import java.awt.Color;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.KeyStroke;
public class ActionFrameTest {
public static void main(String[] args) {
ActionFrame actionFrame = new ActionFrame();
actionFrame.setVisible(true);
}
}
class ActionFrame extends JFrame {
private JPanel buttonPanel;
private static final int DEFALUT_WIDTH = 400;
private static final int DEFAULT_HEIGHT = 300;
public ActionFrame() {
this.setSize(DEFALUT_WIDTH, DEFAULT_HEIGHT);
buttonPanel = new JPanel();
// define actions
// 构造一个动作类的对象。
// 使用动作对象创建按钮或菜单项。
// 构造器将从动作对象中读取标签文本和图标。
Action yellowAction = new ColorAction("Yellow", new ImageIcon("D:\\VSCode\\images\\qunmap-center-icon.png"), Color.YELLOW);
Action blueAction = new ColorAction("Blue", new ImageIcon("D:\\VSCode\\images\\qunmap-center-icon.png"), Color.BLUE);
Action redAction = new ColorAction("Red", new ImageIcon("D:\\VSCode\\images\\qunmap-center-icon.png"), Color.RED);
JButton yellowButton = new JButton(yellowAction);
JButton blueButton = new JButton(blueAction);
JButton redButton = new JButton(redAction);
// redButton.addActionListener(yellowAction);
buttonPanel.add(yellowButton);
buttonPanel.add(blueButton);
buttonPanel.add(redButton);
add(buttonPanel);
// associate the Y, B, R keys with names
/**
* 得到顶层组件的WHEN_ANCESTOR_OF_FOCUS_COMPONENT输入映射。
* 为需要的按键创建一个KeyStrike对象。
* 创建一个描述动作字符串这样的动作键对象。
* 将(按键,动作键)对添加到输入映射中。
*/
InputMap imap = buttonPanel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
imap.put(KeyStroke.getKeyStroke("ctrl Y"), "panel.yellow");
imap.put(KeyStroke.getKeyStroke("ctrl B"), "panel.blue");
imap.put(KeyStroke.getKeyStroke("ctrl R"), "panel.red");
/**
* 得到顶层组件的动作映射。
* 将(动作键,动作对象)添加到映射中。
*/
ActionMap amap = buttonPanel.getActionMap();
amap.put("panel.yellow", yellowAction);
amap.put("panel.red", redAction);
amap.put("panel.blue", blueAction);
}
/**
* 实现一个扩展于AbstractAction类的类。
* 多个相关的动作可以使用同一个类。
*/
private class ColorAction extends AbstractAction {
/**
* Constructs a color action
*
* @param name the name to show onthe button
* @param icon the icon to show on the button
* @param color the background color to change
*/
public ColorAction(String name, Icon icon, Color color) {
putValue(Action.NAME, name);
putValue(Action.SMALL_ICON, icon);
putValue(Action.SHORT_DESCRIPTION, "Set panel color to " + name.toLowerCase());
putValue("color", color);
}
@Override
public void actionPerformed(ActionEvent e) {
Color color = (Color) getValue("color");
buttonPanel.setBackground(color);
}
}
}
3. 鼠标事件
当用户点击鼠标按钮时,将会调用三个监听器方法:鼠标第一次被按下时调用mousePressed ;鼠标被释放时调用mouseReleased ;最后调用mouseClicked。如果只对最终的点击事件感兴趣,就可以忽略前两个方法。用 MouseEvent类对象作为参数,调用getX和 getY方法可以获得鼠标被按下时鼠标指针所在的x和y坐标。要想区分单击、双击和三击(!),需要使用getClickCount方法。
对于鼠标和键盘结合触发的事件,可以采用位掩码来测试已经设置了哪个修饰符。在最初的API中,有两个按钮的掩码与两个键盘修饰符的掩码一样,即
BUTTON2_MASK == ALT_MASK
BUTTON3_MASK == META_MASK
这样做是为了能够让用户使用仅有一个按钮的鼠标通过按下修饰符键来模拟按下其他鼠标键的操作。然而,在Java SE 1.4中,建议使用一种不同的方式。有下列掩码:
BUTTON1_DOWN_MASK
BUTTON2_DOwN_MASK
BUTTON3_DOWN_MASK
SHIFT_DOWN_MASK
CTRL_DOwN_MASK
ALT_DOwN_MASK
ALT_GRAPH_DOWN_MASK
META_DOWN_MASK
getModifiersEx方法能够准确地报告鼠标事件的鼠标按钮和键盘修饰符。
当鼠标在窗口上移动时,窗口将会收到一连串的鼠标移动事件。请注意:有两个独立的接口 MouseListener和 MouseMotionListener。这样做有利于提高效率。当用户移动鼠标时,只关心鼠标点击(clicks)的监听器就不会被多余的鼠标移动(moves)所困扰。
这里给出的测试程序将捕获鼠标动作事件,以便在光标位于一个小方块之上时变成另外一种形状(十字)。实现这项操作需要使用Cursor类中的getPredefinedCursor方法。
如果用户在移动鼠标的同时按下鼠标,就会调用mouseMoved而不是调用mouseDragged。在测试应用程序中,用户可以用光标拖动小方块。在程序中,仅仅用拖动的矩形更新当前光标位置。然后,重新绘制画布,以显示新的鼠标位置。
最后,解释一下如何监听鼠标事件。鼠标点击由mouseClicked过程报告,它是MouseListener接口的一部分。由于大部分应用程序只对鼠标点击感兴趣,而对鼠标移动并不感兴趣,但鼠标移动事件发生的频率又很高,因此将鼠标移动事件与拖动事件定义在一个称为MouseMotionListener的独立接口中。
package Test.Swing;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import javax.swing.JComponent;
import javax.swing.JFrame;
public class MouseFrameTest {
public static void main(String[] args) {
MouseFrame mouseFrame = new MouseFrame();
mouseFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mouseFrame.setVisible(true);
}
}
class MouseFrame extends JFrame {
public MouseFrame() {
add(new MouseComponent());
pack();
}
}
class MouseComponent extends JComponent {
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 200;
private static final int SIDELENGTH = 10;
private final ArrayList<Rectangle2D> squares;
private Rectangle2D current;
public MouseComponent() {
squares = new ArrayList<>();
current = null;
addMouseListener(new MouseHandler());
addMouseMotionListener(new MouseMotionHandler());
}
public Dimension getPreferredSize() {
return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
public void paintComponent(Graphics graphics) {
Graphics2D g2 = (Graphics2D) graphics;
for (Rectangle2D rectangle2d : squares) {
g2.draw(rectangle2d);
}
}
/**
* finds the first square containing a point
*
* @param p a point
* @return the first square that contains p
*/
public Rectangle2D find(Point2D p) {
for (Rectangle2D rectangle2d : squares) {
if (rectangle2d.contains(p))
return rectangle2d;
}
return null;
}
public void add(Point2D point2d) {
double x = point2d.getX();
double y = point2d.getY();
current = new Rectangle2D.Double(x - (double) SIDELENGTH / 2, y - (double) SIDELENGTH / 2, SIDELENGTH,
SIDELENGTH);
squares.add(current);
repaint();
}
public void remove(Rectangle2D rectangle2d) {
if (rectangle2d == null)
return;
if (rectangle2d == current)
current = null;
squares.remove(rectangle2d);
repaint();
}
private class MouseHandler extends MouseAdapter {
@Override
public void mousePressed(MouseEvent event) {
current = find(event.getPoint());
if (current == null)
add(event.getPoint());
}
@Override
public void mouseClicked(MouseEvent e) {
current = find(e.getPoint());
if (current != null && e.getClickCount() >= 2)
remove(current);
}
}
private class MouseMotionHandler implements MouseMotionListener {
@Override
public void mouseMoved(MouseEvent e) {
if (find(e.getPoint()) == null)
setCursor(Cursor.getDefaultCursor());
else
setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
}
@Override
public void mouseDragged(MouseEvent e) {
if (current != null) {
int x = e.getX();
int y = e.getY();
current.setFrame(x - (double) SIDELENGTH / 2, y - (double) SIDELENGTH / 2, SIDELENGTH, SIDELENGTH);
repaint();
}
}
}
}
4. AWT事件继承层次
- 语义事件和底层事件
AWT将事件分为底层( low-level)事件
和语义( semantic)事件
。语义事件是表示用户动作的事件,例如,点击按钮;因此,ActionEvent是一种语义事件。底层事件是形成那些事件的事件。在点击按钮时,包含了按下鼠标、连续移动鼠标、抬起鼠标(只有鼠标在按钮区中抬起才引发)事件。或者在用户利用TAB键选择按钮,并利用空格键激活它时,发生的敲击键盘事件。同样,调节滚动条是一种语义事件,但拖动鼠标是底层事件。
下面是java.awt.event包中最常用的语义事件类:- ActionEvent (对应按钮点击、菜单选择、选择列表项或在文本框中ENTER);
- AdjustmentEvent (用户调节滚动条);
- ItemEvent(用户从复选框或列表框中选择一项)。
常用的5个底层事件类是: - KeyEvent (一个键被按下或释放);
- MouseEvent(鼠标键被按下、释放、移动或拖动);
- MouseWheelEvent (鼠标滚轮被转动);
- FocusEvent(某个组件获得焦点或失去焦点);
- WindowEvent(窗口状态被改变)。
事件处理总结
接口 | 方法 | 参数 | 访问方法 | 事件源 |
---|---|---|---|---|
ActionListener | actionPerformed | ActionEvent | getActionCommand() | AbstractButton |
JComboBox | ||||
getModifiers() | JTextFiled | |||
Timer | ||||
AdjustmentListener | adjustmentVlueChanged | AdjustmentEvent | getAdjustable() | JScrollbar |
getAdjustmentType() | ||||
getValue() | ||||
ItemListener | itemStateChanged | ItemEvent | getItem() | AbstractButton |
getItemSelectable() | ||||
getStateChange() | JComboBox | |||
FocunLinstener | FocusGained | FocusEvent | isTemporary | Component |
FocusLost | ||||
KeyListener | keyPressed | KeyEvent | getKeyChar | Component |
getKeyCode | ||||
keyReleased | getKeyModofiersText | |||
getKeyText | ||||
KeyTyped | isActionKey | |||
MouseListener | mousePressed | MouseEvent | getClickCount | Component |
mouseReleased | getX | |||
mouseEntered | getY | |||
mouseExited | getPoint | |||
mouseClicked | translatePoint | |||
MouseMotionListener | mouseDragged | MouseEvent | Component | |
mouseMoved | ||||
MouseWheelListener | mouseWheelMoved | MouseWheelEvent | getWheelRotation | Component |
getScrollAmount | ||||
WindowListener | windowClosing | WindowEvent | getWindow | Window |
windowOpened | ||||
windowIconified | ||||
windowDeiconified | ||||
windowClosed | ||||
windowActivated | ||||
windowDeactivated | ||||
WindowFocusListener | windowGainedFocus | WindowEvent | getOppisiteWindow | Window |
windowLostFocus | ||||
WindowStateListener | windowStateChanged | WindowEvent | getOldState | Window |
getNewState |
十二、Swing用户界面组件
1. Swing模式-视图-控制器设计模式
-
设计模式
在“模型-视图-控制器”模式中,背景是显示信息和接收用户输入的用户界面系统。
模型-视图-控制器模式并不是AWT 和 Swing 设计中使用的唯一模式。下列是应用的另外几种模式:- 容器和组件是“组合(composite)”模式
- 带滚动条的面板是“装饰器(decorator)”模式
- 布局管理器是“策略(strategy)”模式
-
模型-视图-控制器模式
每个组件都有三要素:- 内容
- 外观
- 行为
模型-视图-控制器(MVC)遵循面向对象设计中的一个基本原则:限制一个对象拥有的功能数量。不要用一个按钮类完成所有的事情,而是应该让一个对象负责组件的观感,另一个对象负责存储内容。模型–视图-控制器(MVC)模式告诉我们如何实现这种设计,实现三个独立的类:
- 模型(model):存储内容。
- 视图(view):显示内容。
- 控制器( controller):处理用户输入。
-
Swing按钮的MVC模式分析
对于大多数组件来说,模型类将实现一个名字以Model结尾的接口,例如,按钮就实现了ButtonModel接口。实现了此接口的类可以定义各种按钮的状态。实际上,按钮并不复杂,在Swing 库中有一个名为DefaultButtonModel的类就实现了这个接口。接口属性:
protected String actionCommand //The action command string fired by the button.(与按钮关联的动作字符串命令) static int ARMED //Identifies the "armed" bit in the bitmask, which indicates partial commitment towards choosing/triggering the button.(如果按钮被按下且鼠标仍在按钮上则为true) protected ChangeEvent changeEvent //Only one ChangeEvent is needed per button model instance since the event's only state is the source property. static int ENABLED //Identifies the "enabled" bit in the bitmask, which indicates that the button can be selected by an input device (such as a mouse pointer).(如果按钮是可选择的则为true) protected ButtonGroup group //The button group that the button belongs to. protected EventListenerList listenerList //Stores the listeners on this model. protected int mnemonic //The button's mnemonic.(按钮的快捷键) static int PRESSED //Identifies the "pressed" bit in the bitmask, which indicates that the button is pressed.(如果按钮被按下且鼠标还没有释放则为true) static int ROLLOVER //Identifies the "rollover" bit in the bitmask, which indicates that the mouse is over the button.(如果鼠标在按钮之上滋味true) static int SELECTED //Identifies the "selected" bit in the bitmask, which indicates that the button has been selected.(如果按钮已经被选择【用于复选框和单选钮】则为true) protected int stateMask //The bitmask used to store the state of the button.
2. 布局管理概述
通常,组件放置在容器中,布局管理器决定容器中的组件具体放置的位置和大小。
按钮、文本域和其他的用户界面元素都继承于Component类,组件可以放置在面板这样的容器中。由于Container类继承于Component类,所以容器也可以放置在另一个容器中。
每个容器都有一个默认的布局管理器,但可以重新进行设置。例如,使用下列语句:
panel.setLayout(new GridLayout(4,4));
这个面板将用GridLayout类布局组件。可以往容器中添加组件。容器的add方法将把练件和放置的方位传递给布局管理器。
-
边框布局
边框布局管理器( border layout manager)是每个JFrame的内容窗格的默认布局管理器。流布局管理器完全控制每个组件的放置位置,边框布局管理器则不然,它允许为每个组件选择一个放置位置。可以选择把组件放在内容窗格的中部、北部、南部、东部或者西部。例如:
frame.add(component,BorderLayout.SOUTH);
先放置边缘组件,剩余的可用空间由中间组件占据。当边框布局容器被缩放时,边缘组件的尺寸不会改变,而中部组件的大小会发生变化。在添加组件时可以指定 BorderLayout类中的CENTER、NORTH、SOUTH、EAST和 WEST常量。并非需要占用所有的位置,如果没有提供任何值,系统默认为CENTER。
与流布局不同,边框布局会扩展所有组件的尺寸以便填满可用空间(流布局将维持每个组件的最佳尺寸)。当将一个按钮添加到容器中时会出现问题:
frame.add(yellowButton, BorderLayout.SOUTH);
图12-10给出了执行上述语句的显示效果。按钮扩展至填满框架的整个南部区域。而且,如果再将另外一个按钮添加到南部区域,就会取代第一个按钮。
解决这个问题的常见方法是使用另外一个面板( panel)。例如,如图12-11所示。屏幕底部的三个按钮全部包含在一个面板中。这个面板被放置在内容窗格的南部。
要想得到这种配置效果,首先需要创建一个新的JPanel对象,然后逐一将按钮添加到面板中。面板的默认布局管理器是FlowLayout,这恰好符合我们的需求。随后使用在前面已经看到的add方法将每个按钮添加到面板中。每个按钮的放置位置和尺寸完全处于FlowLayout布局管理器的控制之下。这意味着这些按钮将置于面板的中央,并且不会扩展至填满整个面板区域。最后,将这个面板添加到框架的内容窗格中。
JPanel panel = new JPane1();
panel.add(yel1owButton);
panel .add(blueButton);
pane1.add(redButton) ;
frame.add(pane1,BorderLayout.SOUTH);
边框布局管理器将会扩展面板大小,直至填满整个南部区域。 -
网格布局
网格布局像电子数据表一样,按行列排列所有的组件。不过,它的每个单元大小都是一样的。
在网格布局对象的构造器中,需要指定行数和列数:
panel.setLayout(new GridLayout(4,4));
添加组件,从第一行的第一列开始,然后是第一行的第二列,以此类推。
pane1.add(new JButton(“1”));
panel.add(new JButton(“2”));
package Test.Swing;
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class CalculatorPanel extends JPanel {
/**
* 结果显示
*/
private final JButton display;
/**
* 按钮布局
*/
private final JButton panel;
/**
* 计算结果
*/
private double result;
/**
* 运算符
*/
private String lastCommand;
/**
* 是否开始计算
*/
private boolean start;
public CalculatorPanel() {
setLayout(new BorderLayout());
result = 0;
lastCommand = "=";
start = true;
display = new JButton("0");
display.setEnabled(false);
add(display, BorderLayout.NORTH);
ActionListener insert = new InsertAction();
ActionListener command = new CommandAction();
panel = new JButton();
panel.setLayout(new GridLayout(4, 4));
addButton("7", insert);
addButton("8", insert);
addButton("9", insert);
addButton("/", command);
addButton("4", insert);
addButton("5", insert);
addButton("6", insert);
addButton("*", command);
addButton("1", insert);
addButton("2", insert);
addButton("3", insert);
addButton("-", command);
addButton("0", insert);
addButton(".", insert);
addButton("=", command);
addButton("+", insert);
add(panel, BorderLayout.CENTER);
}
private void addButton(String label, ActionListener listener) {
JButton button = new JButton(label);
button.addActionListener(listener);
panel.add(button);
}
private void calculate(double parseDouble) {
switch (lastCommand) {
case "+":
result += parseDouble;
break;
case "-":
result -= parseDouble;
break;
case "*":
result *= parseDouble;
break;
case "/":
result /= parseDouble;
break;
case "=":
result = parseDouble;
break;
}
display.setText(String.valueOf(result));
}
private class InsertAction implements ActionListener {
@Override
public void actionPerformed(ActionEvent event) {
String input = event.getActionCommand();
if (start) {
display.setText("");
start = false;
}
display.setText(display.getText() + input);
}
}
private class CommandAction implements ActionListener {
@Override
public void actionPerformed(ActionEvent event) {
String command = event.getActionCommand();
if (start) {
if (command.equals("-")) {
display.setText(command);
start = false;
} else {
lastCommand = command;
}
} else {
calculate(Double.parseDouble(display.getText()));
lastCommand = command;
start = true;
}
}
}
}
class CalculatorFrame extends JFrame {
private static final int DEFAULT_WIDTH = 900;
private static final int DEFAULT_HEIGHT = 1000;
public CalculatorFrame() {
add(new CalculatorPanel());
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
pack();
}
public static void main(String[] args) {
CalculatorFrame calculatorFrame = new CalculatorFrame();
calculatorFrame.setVisible(true);
}
}
3. 文本输入
首先,介绍具有用户输入和编辑文本功能的组件。文本域(JTextField)和文本区(JTextArea)组件用于获取文本输入。文本域只能接收单行文本的输入,而文本区能够接收多行文本的输入。JPassword 也只能接收单行文本的输入,但不会将输入的内容显示出来。
这三个类都继承于JTextComponent类。由于JTextComponent是一个抽象类,所以不能够构造这个类的对象。另外,在Java中常会看到这种情况。在查看API文档时,发现自己正在寻找的方法实际上来自父类JTextComponent,而不是来自派生类自身。例如,在一个文本域和文本区内获取(get)、设置(set)文本的方法实际上都是JTextComponent类中的方法。
-
文本域
把文本域添加到窗口的常用办法是将它添加到面板或者其他容器中,这与添加按钮完全一样:
JPanel panel = new ]Pane1O;
JTextField textField = new JTextField(“Default input”,20);
panel.add(textField);
pane1.add(textField);
这段代码将添加一个文本域,同时通过传递字符串“Default input”进行初始化。构造器的第二个参数设置了文本域的宽度。在这个示例中,宽度值为20“列”。但是,这里所说的列不是一个精确的测量单位。一列就是在当前使用的字体下一个字符的宽度。如果希望文本域最多能够输人n个字符,就应该把宽度设置为n列。在实际中,这样做效果并不理想。如果需要在运行时重新设置列数,可以使用setColumns方法。提示:使用setColumns方法改变了一个文本域的大小之后,需要调用包含这个文本框的容器的revalidate方法.
textField.setColumns(10);pane1.revalidateO;
revalidate方法会重新计算容器内所有组件的大小,并且对它们重新进行布局。调用revalidate方法以后,布局管理器会重新设置容器的大小,然后就可以看到改变尺寸后的文本域了。可以在任何时候调用setText方法改变文本域中的内容。
如果想要改变显示文本的字体,就调用setFont方法。 -
标签和标签组件
标签是容纳文本的组件,它们没有任何的修饰(例如没有边缘),也不能响应用户输入。可以利用标签标识组件。例如:与按钮不同,文本域没有标识它们的标签。要想用标识符标识这种不带标签的组件,应该- 用相应的文本构造一个JLabel组件。
- 将标签组件放置在距离需要标识的组件足够近的地方,以便用户可以知道标签所标识的组件。
JLabel 的构造器允许指定初始文本和图标,也可以选择内容的排列方式。可以用SwingConstants接口中的常量来指定排列方式。在这个接口中定义了几个很有用的常量,如LEFT,RIGHT、CENTER、NORTH、EAST等。JLabel是实现这个接口的一个Swing类。因此,可以指定右对齐标签:
JLabel label = new JLabe1(“User name:”,SwingConstants.RIGHT);
或者
JLabel label = new JLabe1 ("User name: ",]Label.RIGHT);
利用setText和 setlcon方法可以在运行期间设置标签的文本和图标。 -
密码域
密码域是一种特殊类型的文本域。为了避免有不良企图的人看到密码,用户输入的字符不显示出来。每个输人的字符都用回显字符( echo character)表示,典型的回显字符是星号(*)。Swing提供了JPasswordField类来实现这样的文本域。 -
文本区
有时,用户的输入超过一行。正像前面提到的,需要使用JTextArea组件来接收这样的输入。当在程序中放置一个文本区组件时,用户就可以输入多行文本,并用ENTER键换行。每行都以一个“\n”结尾。
如果文本区的文本超出显示的范围,那么剩下的文本就会被剪裁掉。可以通过开启换行特性来避免裁剪过长的行:
textArea.setLinewrap(true);// long lines are wrapped -
滚动窗格
在Swing中,文本区没有滚动条。如果需要滚动条,可以将文本区插入到滚动窗格(scroll pane)中。
textArea = new JTextArea(8,40);
JScro11 Pane scro1lPane = new JScrollPane(textArea);
现在滚动窗格管理文本区的视图。如果文本超出了文本区可以显示的范围,滚动条就会自动地出现,并且在删除部分文本后,当文本能够显示在文本区范围内时,滚动条会再次自动地消失。滚动是由滚动窗格内部处理的,编写程序时无需处理滚动事件。
这是一种为任意组件添加滚动功能的通用机制,而不是文本区特有的。也就是说,要想为组件添加滚动条,只需将它们放入一个滚动窗格中即可。
package Test.Swing;
import java.awt.BorderLayout;
import java.awt.GridLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class TextComponentFrameTest {
public static void main(String[] args) {
try {
UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel");
TextComponentFrame textComponentFrame = new TextComponentFrame();
textComponentFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
textComponentFrame.setVisible(true);
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e) {
e.printStackTrace();
}
}
}
class TextComponentFrame extends JFrame {
public static final int TEXTAREA_ROWS = 8;
public static final int TEXTAREA_COLS = 20;
public TextComponentFrame() {
JTextField textField = new JTextField();
JPasswordField passwordField = new JPasswordField();
JPanel northPanel = new JPanel();
northPanel.setLayout(new GridLayout(2, 2));
northPanel.add(new JLabel("User name: ", SwingConstants.RIGHT));
northPanel.add(textField);
northPanel.add(new JLabel("Password: ", SwingConstants.RIGHT));
northPanel.add(passwordField);
add(northPanel, BorderLayout.NORTH);
JTextArea textArea = new JTextArea(TEXTAREA_ROWS, TEXTAREA_COLS);
JScrollPane scrollPane = new JScrollPane(textArea);
JPanel southPanel = new JPanel();
add(scrollPane, BorderLayout.SOUTH);
JButton insertButton = new JButton("finish");
insertButton.addActionListener(event -> {
textArea.append("User name: " + textField.getText() + "\tPassword: " + new String(passwordField.getPassword()) + "\n");
});
southPanel.add(insertButton);
add(southPanel, BorderLayout.CENTER);
pack();
}
}
4. 选择组件
- 复选框
如果想要接收的输人只是“是”或“非”,就可以使用复选框组件。复选框自动地带有标识标签。用户通过点击某个复选框来选择相应的选项,再点击则取消选取。当复选框获得焦点时,用户也可以通过按空格键来切换选择。
复选框需要一个紧邻它的标签来说明其用途。在构造器中指定标签文本。
bold = new JCheckBox(“Bold”);
可以使用setSelected方法来选定或取消选定复选框。例如:
bold.setSelected(true);
isSelected方法将返回每个复选框的当前状态。如果没有选取则为false,否则为true.
package Test.Swing;
import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.event.ActionListener;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
public class CheckBoxTest {
public static void main(String[] args) {
CheckBoxFrame checkBoxFrame = new CheckBoxFrame();
checkBoxFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
checkBoxFrame.setVisible(true);
}
}
class CheckBoxFrame extends JFrame {
private static final int FONTSIZE = 24;
private JLabel label;
private JCheckBox bold;
private JCheckBox italic;
private JCheckBox plain;
public CheckBoxFrame() {
label = new JLabel("满船星河压星梦!");
label.setFont(new Font("Serif", Font.BOLD, FONTSIZE));
add(label, BorderLayout.CENTER);
ActionListener listener = event -> {
int mode = 0;
if (bold.isSelected())
mode = Font.BOLD;
else if (italic.isSelected())
mode = Font.ITALIC;
else if (plain.isSelected())
mode = Font.PLAIN;
label.setFont(new Font("Serif", mode, FONTSIZE));
};
bold = new JCheckBox("Bold");
bold.setSelected(true);
bold.addActionListener(listener);
italic = new JCheckBox("Italic");
italic.addActionListener(listener);
plain = new JCheckBox("Plain");
plain.addActionListener(listener);
JPanel panel = new JPanel();
panel.add(bold);
panel.add(italic);
panel.add(plain);
add(panel, BorderLayout.SOUTH);
pack();
}
}
-
单选钮
在Swing 中,实现单选钮组非常简单。为单选钮组构造一个 ButtonGroup的对象。然后,再将JRadioButton类型的对象添加到按钮组中。按钮组负责在新按钮被按下时,取消前一个被按下的按钮的选择状态。
构造器的第二个参数为true表明这个按钮初始状态是被选择,其他按钮构造器的这个参数为false。注意,按钮组仅仅控制按钮的行为,如果想把这些按钮组织在一起布局,需要把它们添加到容器中,如JPanel。 -
边框
如果在一个窗口中有多组单选按钮,就需要用可视化的形式指明哪些按钮属于同一组。Swing提供了一组很有用的边框( borders)来解决这个问题。可以在任何继承了JComponent的组件上应用边框。最常用的用途是在一个面板周围放置一个边框,然后用其他用户界面元素(如单选钮)填充面板。
有几种不同的边框可供选择,但是使用它们的步骤完全一样。- 调用BorderFactory 的静态方法创建边框。下面是几种
- 凹斜面
- 凸斜面
- 蚀刻
- 直线
- 蒙版
- 空(只是在组件外围创建一些空白空间)
- 如果愿意的话,可以给边框添加标题,具体的实现方法是将边框传递给BroderFactory.createTitledBorder。
- 如果确实想把一切凸显出来,可以调用下列方法将几种边框组合起来使用:BorderFactory.createCompoundBorder 。
- 调用JComponent类中 setBorder 方法将结果边框添加到组件中。
- 调用BorderFactory 的静态方法创建边框。下面是几种
package Test.Swing;
import java.awt.Color;
import java.awt.GridLayout;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.UIManager;
import javax.swing.border.Border;
public class BorderFrameTest {
public static void main(String[] args) {
try {
UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel");
} catch (Exception e) {
e.printStackTrace();
}
BorderFrame borderFrame = new BorderFrame();
borderFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
borderFrame.setVisible(true);
}
}
class BorderFrame extends JFrame {
private final JPanel demoPanel;
private final JPanel buttonPanel;
private final ButtonGroup group;
public BorderFrame() {
demoPanel = new JPanel();
buttonPanel = new JPanel();
group = new ButtonGroup();
addRadioButton("Lowered bevel", BorderFactory.createLoweredBevelBorder());
addRadioButton("Raised bevel", BorderFactory.createRaisedBevelBorder());
addRadioButton("Etched", BorderFactory.createEtchedBorder());
addRadioButton("Line", BorderFactory.createLineBorder(Color.RED));
addRadioButton("Matte", BorderFactory.createMatteBorder(10, 10, 10, 10, Color.BLUE));
addRadioButton("Empty", BorderFactory.createEmptyBorder());
Border etched = BorderFactory.createEtchedBorder();
Border titled = BorderFactory.createTitledBorder(etched, "Border Types");
buttonPanel.setBorder(titled);
setLayout(new GridLayout(2, 1));
this.add(buttonPanel);
add(demoPanel);
pack();
}
private void addRadioButton(String buttonName, Border border) {
JRadioButton button = new JRadioButton(buttonName);
button.addActionListener(e -> demoPanel.setBorder(border));
group.add(button);
buttonPanel.add(button);
}
}
- 组合框
在Java SE 7中,JComboBox类是一个泛型类。例如,JComboBox包含String类型的对象,JComboBox包含整数。
调用setEditable方法可以让组合框可编辑。注意,编辑只会影响当前项,而不会改变列表内容。
可以调用getSelectedltem方法获取当前的选项,如果组合框是可编辑的,当前选项则是可以编辑的。不过,对于可编辑组合框,其中的选项可以是任何类型,这取决于编辑器(即由编辑器获取用户输入并将结果转换为一个对象)。如果你的组合框不是可编辑的,最好调用
combo.getItemAt(combo.getSelectedIndex())
这会为所选选项提供正确的类型。
可以调用addItem方法增加选项。
这个方法将字符串添加到列表的尾部。可以利用insertItemAt方法在列表的任何位置插入一个新选项:
可以增加任何类型的选项,组合框可以调用每个选项的 toString 方法显示其内容。
如果需要在运行时删除某些选项,可以使用removeltem或者removeItemAt方法,使用哪个方法将取决于参数提供的是想要删除的选项内容,还是选项位置。
当用户从组合框中选择一个选项时,组合框就将产生一个动作事件。为了判断哪个选项被选择,可以通过事件参数调用getSource方法来得到发送事件的组合框引用,接着调用getSelectedltem方法获取当前选择的选项。需要把这个方法的返回值转化为相应的类型,通常是 String型。
package com.company.Test.Swing;
import com.company.ChangeDefaultUI;
import javax.swing.*;
import java.awt.BorderLayout;
import java.awt.Font;
public class ComboBoxFrameTest {
public static void main(String[] args) {
ChangeDefaultUI.changeDefaultUI();
ComboBoxFrame comboBoxFrame = new ComboBoxFrame();
// comboBoxFrame.setSize(300,200);
comboBoxFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
comboBoxFrame.setVisible(true);
}
}
class ComboBoxFrame extends JFrame {
private final JComboBox<String> faceCombo;
private final JLabel label;
private static final int DEFAULT_SIZE = 24;
public ComboBoxFrame() {
label = new JLabel("THE QUICK FOX JUMPS OVER THE LAZY DOG!");
label.setFont(new Font("Serif", Font.PLAIN,DEFAULT_SIZE));
JPanel northPanel = new JPanel();
northPanel.add(label);
add(northPanel, BorderLayout.NORTH);
faceCombo = new JComboBox<>();
faceCombo.addItem("Serif");
faceCombo.addItem("SanSerif");
faceCombo.addItem("MonoSpaced");
faceCombo.addItem("Dialog");
faceCombo.addItem("DialogInput");
faceCombo.addActionListener(e -> {
label.setFont(new Font(faceCombo.getItemAt(faceCombo.getSelectedIndex()),Font.PLAIN,DEFAULT_SIZE));
northPanel.revalidate();
this.validate();
});
JPanel comboPanel = new JPanel();
comboPanel.add(faceCombo);
add(comboPanel,BorderLayout.SOUTH);
pack();
}
}
- 滑动条JSlider
组合框可以让用户从一组离散值中进行选择。滑动条允许进行连续值的选择,例如,从1 ~100之间选择任意数值。
通常,可以使用下列方式构造滑动条:
JSlider slider = new JSlider(min,max,initialValue);
如果省略最小值、最大值和初始值,其默认值分别为0、100和50。
或者如果需要垂直滑动条,可以按照下列方式调用构造器:
JSlider slider = new JSlider(SwingConstants.VERTICAL,min,max,initiaValue);
这些构造器构造了一个无格式的滑动条,
下面看一下如何为滑动条添加装饰。当用户滑动滑动条时,滑动条的值就会在最小值和最大值之间变化。当值发生变化时,ChangeEvent就会发送给所有变化的监听器。为了得到这些改变的通知,需要调用addChangeListener方法并且安装一个实现了ChangeListener接口的对象。这个接口只有一个方法StateChanged。在这个方法中,可以获取滑动条的当前值:
ChangeListener listener = event-> {
JSlider slider = (JSlider) event.getSource();
int value = slider.getValue();
};
可以通过显示标尺( tick)对滑动条进行修饰。
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
这些代码只设置了标尺标记,要想将它们显示出来,还需要调用:
slider.setPaintTicks(true);
可以强制滑动条对齐标尺。这样一来,只要用户完成拖放滑动条的操作,滑动条就会立即自动地移到最接近的标尺处。激活这种操作方式需要调用:
s1ider.setSnapToTicks(true);
package Test.Swing;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridLayout;
import java.util.Dictionary;
import java.util.Hashtable;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JTextField;
import javax.swing.event.ChangeListener;
import Test.ChangeDefaultUI;
public class SliderFrameTest {
public static void main(String[] args) {
ChangeDefaultUI.changeDefaultUI();
SliderFrame sliderFrame = new SliderFrame();
sliderFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
sliderFrame.setVisible(true);
}
}
class SliderFrame extends JFrame {
private final JPanel sliderPanel;
private final ChangeListener listener;
private JTextField textField;
public SliderFrame() {
sliderPanel = new JPanel();
sliderPanel.setLayout(new GridLayout());
listener = event -> {
JSlider source = (JSlider) event.getSource();
textField.setText("" + source.getValue());
};
JSlider slider = new JSlider();
addSlider(slider, "Plain");
slider = new JSlider();
slider.setPaintTicks(true);
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
addSlider(slider, "Ticks");
slider = new JSlider();
slider.setPaintTicks(true);
slider.setSnapToTicks(true);
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
addSlider(slider, "Snap To Ticks");
slider = new JSlider();
slider.setPaintTicks(true);
slider.setPaintTrack(false);
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
addSlider(slider, "No Track");
slider = new JSlider();
slider.setPaintTicks(true);
slider.setInverted(true);
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
addSlider(slider, "Inverted");
slider = new JSlider();
slider.setPaintTicks(true);
slider.setPaintLabels(true);
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
addSlider(slider, "Labels");
slider = new JSlider();
slider.setPaintTicks(true);
slider.setPaintLabels(true);
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
Dictionary<Integer, Component> labelTable = new Hashtable<>();
for (int i = 0; i < 6; i++) {
char ch = (char) (i + 65);
labelTable.put(i * 20, new JLabel(String.valueOf(ch)));
}
slider.setLabelTable(labelTable);
addSlider(slider, "Custom labels");
slider = new JSlider();
slider.setPaintTicks(true);
slider.setPaintLabels(true);
slider.setSnapToTicks(true);
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
labelTable = new Hashtable<>();
for (int i = 0; i < 6; i++) {
labelTable.put(i * 20, new JLabel(new ImageIcon("D:\\VSCode\\images\\qunmap-center-icon.png")));
}
slider.setLabelTable(labelTable);
addSlider(slider, "Icon Labels");
textField = new JTextField();
add(sliderPanel, BorderLayout.CENTER);
add(textField, BorderLayout.SOUTH);
pack();
}
private void addSlider(JSlider slider, String description) {
slider.addChangeListener(listener);
JPanel jPanel = new JPanel();
jPanel.add(slider);
jPanel.add(new JLabel(description));
jPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
GridBagConstraints gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridy = sliderPanel.getComponentCount();
gridBagConstraints.anchor = GridBagConstraints.WEST;
sliderPanel.add(jPanel, gridBagConstraints);
}
}
5. 菜单
-
菜单创建
创建菜单是一件非常容易的事情。首先要创建一个菜单栏:
JMenuBar menuBar = new JMenuBar();
在框架的顶部。可以调用setJMenuBar 方法将菜单栏添加到框架上:
frame.setJMenuBar(menuBar);
需要为每个菜单建立一个菜单对象:
JMenu editMenu = new ]Menu(“Edit”);
然后将顶层菜单添加到菜单栏中:
menuBar.add(editMenu);当用户选择菜单时,将触发一个动作事件。这里需要为每个菜单项安装一个动作监听器。
-
菜单项中的图标
菜单项与按钮很相似。实际上,JMenultem类扩展了AbstractButton类。与按钮一样,菜单可以包含文本标签、图标,也可以两者都包含。既可以利用JMenultem(String, Icon)或者JMenultem(Icon)构造器为菜单指定一个图标,也可以利用JMenultem类中的setIcon方法(继承自AbstractButton类)指定一个图标。 -
复选框和单选钮菜单项
复选框和单选钮菜单项在文本旁边显示了一个复选框或一个单选钮。当用户选择一个菜单项时,菜单项就会自动地在选择和未选择间进行切换。
除了按钮装饰外,同其他菜单项的处理一样。例如,下面是创建复选框菜单项的代码:
JCheckBoxMenuItem readonlyItem = new CheckBoxMenuItem(“Read-only”);
optionsMenu.add(readonlyItem);单选钮菜单项与普通单选钮的工作方式一样,必须将它们加入到按钮组中。当按钮组中的一个按钮被选中时,其他按钮都自动地变为未选择项。
-
弹出菜单
弹出菜单(pop-up menu)是不固定在菜单栏中随处浮动的菜单。
创建一个弹出菜单与创建一个常规菜单的方法类似,但是弹出菜单没有标题。
JPopupMenu popup = new JPopupNenu();
然后用常规的方法添加菜单项:
JMenultem item = new JMenuItem(“Cut”);
item.addActionlistener(1istener);
popup.add(item);
弹出菜单并不像常规菜单栏那样总是显示在框架的顶部,必须调用show方法菜单才能显示出来。调用时需要给出父组件以及相对父组件坐标的显示位置。例如:
popup.show(panel,x,y);通常,当用户点击某个鼠标键时弹出菜单。这就是所谓的弹出式触发器( pop-uptrigger)。在 Windows或者Linux中,弹出式触发器是鼠标右键。要想在用户点击某一个组件时弹出菜单,需要按照下列方式调用方法:
component.setComponentPopupMenu(popup) ;
偶尔会遇到在一个含有弹出菜单的组件中放置一个组件的情况。这个子组件可以调用下列方法继承父组件的弹出菜单。调用:
child.setInheritsPopupMenu(true); -
快捷键和加速器
对于有经验的用户来说,通过快捷键来选择菜单项会感觉更加便捷。可以通过在菜单项的构造器中指定一个快捷字母来为菜单项设置快捷键:
JMenuItem aboutItem = new JMenuItem(“About”,“A”);
只能在菜单项的构造器中设定快捷键字母,而不是在菜单构造器中。如果想为菜单设i快捷键,需要调用setMnemonic方法:
JMenu helpMenu = new JMenu(“Help”);
he1pMenu.setMnemonic(‘H’);可以使用快捷键从当前打开的菜单中选择一个子菜单或者菜单项。而加速器是在不打开菜单的情况下选择菜单项的快捷键。例如:很多程序把加速器CTRL+O和CTRL+S关联到File菜单中的Open和 Save菜单项。可以使用setAccelerator将加速器键关联到一个菜单项上。这个方法使用KeyStroke类型的对象作为参数。例如:下面的调用将加速器CTRL+O关联到OpenItem菜单项。
openItem.setAccelerator(KeyStroke.getKeyStroke(“ctr1 O”)); -
启用和禁用菜单项
启用或禁用菜单项需要调用setEnabled方法:saveItem.setEnabled(false);
启用和禁用菜单项有两种策略。每次环境发生变化就对相关的菜单项或动作调用setEnabled。例如:只要当文档以只读方式打开,就禁用Save和 Save As菜单项。另一种方法是在显示菜单之前禁用这些菜单项。这里必须为“菜单选中”事件注册监听器。javax.swing.event包定义了MenuListener接口,它包含三个方法:
void menuSelected (MenuEvent event)
void menuDeselected(MenuEvent event)void menuCanceled (MenuEvent event)
由于在菜单显示之前调用menuSelected方法,所以可以在这个方法中禁用或启用菜单项。下面代码显示了只读复选框菜单项被选择以后,如何禁用Save和 Save As动作。public void menuSelected(MenuEvent event){ saveAction.setEnabled(!readonlyItem.isSelected()); saveAsAction.setEnabled(!readonlyItem.isSelectedO); }
警告:在显示菜单之前禁用菜单项是一种明智的选择,但这种方式不适用于带有加速键的菜单项。这是因为在按下加速键时并没有打开菜单,因此动作没有被禁用,致使加速键还会触发这个行为。
package Test.Swing;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ButtonGroup;
import javax.swing.ImageIcon;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.KeyStroke;
import Test.ChangeDefaultUI;
public class MenuFrameTest {
public static void main(String[] args) {
ChangeDefaultUI.changeDefaultUI();
MenuFrame menuFrame = new MenuFrame();
menuFrame.setVisible(true);
}
}
class MenuFrame extends JFrame {
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 200;
private Action saveAction;
private Action saveAsAction;
private JCheckBoxMenuItem readOnlyItem;
private JPopupMenu popupMenu;
class PrintAction extends AbstractAction {
public PrintAction(String name) {
super(name);
}
@Override
public void actionPerformed(ActionEvent e) {
System.out.println(getValue(Action.NAME) + "selected.");
}
}
public MenuFrame() {
setSize(new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT));
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JMenu fileMenu = new JMenu("File");
fileMenu.add(new PrintAction("New"));
JMenuItem openItem = fileMenu.add(new PrintAction("Open"));
openItem.setAccelerator(KeyStroke.getKeyStroke("ctrl O"));
fileMenu.addSeparator();
saveAction = new PrintAction("Save");
JMenuItem saveItem = fileMenu.add(saveAction);
saveItem.setAccelerator(KeyStroke.getKeyStroke("ctrl S"));
saveAsAction = new PrintAction("Save As");
fileMenu.add(saveAsAction);
fileMenu.addSeparator();
fileMenu.add(new AbstractAction("Exit") {
@Override
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
});
readOnlyItem = new JCheckBoxMenuItem("Read-Only");
readOnlyItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
saveAction.setEnabled(!readOnlyItem.isSelected());
saveAsAction.setEnabled(!readOnlyItem.isSelected());
}
});
ButtonGroup buttonGroup = new ButtonGroup();
JRadioButtonMenuItem insertItem = new JRadioButtonMenuItem("Insert");
insertItem.setSelected(true);
JRadioButtonMenuItem overTypeItem = new JRadioButtonMenuItem("OverType");
buttonGroup.add(insertItem);
buttonGroup.add(overTypeItem);
Action cutAction = new PrintAction("Cut");
cutAction.putValue(Action.SMALL_ICON, new ImageIcon("D:\\VSCode\\images\\qunmap-center-icon.png"));
Action copyAction = new PrintAction("Copy");
copyAction.putValue(Action.SMALL_ICON, new ImageIcon("D:\\VSCode\\images\\qunmap-center-icon.png"));
Action pasteAction = new PrintAction("Paste");
pasteAction.putValue(Action.SMALL_ICON, new ImageIcon("D:\\VSCode\\images\\qunmap-center-icon.png"));
JMenu editMenu = new JMenu("Edit");
editMenu.add(cutAction);
editMenu.add(copyAction);
editMenu.add(pasteAction);
JMenu optionMenu = new JMenu("Option");
optionMenu.add(readOnlyItem);
optionMenu.addSeparator();
optionMenu.add(insertItem);
optionMenu.add(overTypeItem);
editMenu.addSeparator();
editMenu.add(optionMenu);
JMenu helpMenu = new JMenu("Help");
helpMenu.setMnemonic('H');
JMenuItem indexItem = helpMenu.add(new PrintAction("Index"));
indexItem.setMnemonic('I');
Action aboutAction = new PrintAction("About");
aboutAction.putValue(Action.MNEMONIC_KEY, new Integer('A'));
// helpMenu.add(indexItem);
helpMenu.add(aboutAction);
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
menuBar.add(fileMenu);
menuBar.add(editMenu);
menuBar.add(helpMenu);
popupMenu = new JPopupMenu();
popupMenu.add(cutAction);
popupMenu.add(copyAction);
popupMenu.add(pasteAction);
JPanel panel = new JPanel();
panel.setComponentPopupMenu(popupMenu);
add(panel);
}
}
-
工具栏
工具栏是在程序中提供的快速访问常用命令的按钮栏。
工具栏的特殊之处在于可以将它随处移动。可以将它拖拽到框架的四个边框上。释放鼠标按钮后,工具栏将会停靠在新的位置上。注释:工具栏只有位于采用边框布局或者任何支持North、East、South和West约束布局管理器的容器内才能够被拖拽。
编写创建工具栏的代码非常容易,并且可以将组件添加到工具栏中:
JToolBar bar = new JToolBar();
bar.add(blueButton);
JToolBar类还有一个用来添加Action对象的方法,可以用Action对象填充工具栏:
bar.add(blueAction);
这个动作的小图标将会出现在工具栏中。
可以用分隔符将按钮分组:
bar.addSeparator();
将工具栏添加到框架中:
add(bar,BorderLayout.NORTH);
当工具栏没有停靠时,可以指定工具栏的标题:
bar = new JToolBar(titleString);
在默认情况下,工具栏最初为水平的。如果想要将工具栏垂直放置,可以使用下列代码
bar = new JToolBar(SwingConstants.VERTICAL)
或者
bar = new JToolBar(titleString,SwingConstants.VERTICAL); -
工具提示
在 Swing 中,可以调用setToolText方法将工具提示添加到JComponent 上:
exitButton.setToolTipText(“Exit”);
还有一种方法是,如果使用Action对象,就可以用SHORT_DESCRIPTION关联工具提示:
exitAction.putValue(Action. SHORT_DESCRIPTION,“Exit”);注意:动作名在菜单中就是菜单项名,而在工具栏中就是简短的说明。
6. 复杂的布局管理
- 网格组布局(GridBagLayout)
网格组布局是所有布局管理器之母。可以将网格组布局看成是没有任何限制的网格布局。在网格组布局中,行和列的尺寸可以改变。可以将相邻的单元合并以适应较大的组件(很多字处理器以及HTML都利用这个功能编辑表格:一旦需要就合并相邻的单元格)。组件不需要填充整个单元格区域,并可以指定它们在单元格内的对齐方式。
要想使用网格组管理器进行布局,必须经过下列过程:-
建立一个GridBagLayout的对象。不需要指定网格的行数和列数。布局管理器会根据后面所给的信息猜测出来。
GridBagLayout layout = new GridBagLayout(); -
将GridBagLayout对象设置成组件的布局管理器。
panel.setLayout(layout); -
为每个组件建立一个 GridBagConstraints对象。设置GridBagConstraints对象的域以便指出组件在网格组中的布局方案。
GridBagConstraints constraints = new GrigBagConstraints();
constraints.weightx = 100;
constraints.weighty = 100;
constraints.gridx = 0;
constraints.gridy = 2;
constraints.gridwidth = 2;
constraints.gridheight = 1; -
最后,通过下面的调用添加组件的约束:
panel.add(component,constraints ) ; -
gridx、gridy、gridwidth、gridheight参数
这些约束定义了组件在网格中的位置。gridx和 gridy指定了被添加组件左上角的行、列位置。gridwidth和 gridheight 指定了组件占据的行数和列数。 -
增量域
在网格布局中,需要为每个区域设置增量域( weightx和 weighty)。如果将增量设置为0,则这个区域将永远为初始尺寸。另一方面,如果将所有区域的增量都设置为0,容器就会集聚在为它分配的区域中间,而不是通过拉伸来填充它。
从概念上讲,增量参数属于行和列的属性,而不属于某个单独的单元格。但却需要在单元格上指定它们,这是因为网格组布局并不暴露行和列。行和列的增量等于每行或每列单元格的增量最大值。因此,如果想让一行或一列的大小保持不变,就需要将这行、这列的所有组件的增量都设置为0。 -
fill和anchor参数
如果不希望组件拉伸至整个区域,就需要设置fill约束。它有四个有效值: GridBagConstraints.NONE、GridBagConstraints.HORIZONTAL、GridBagConstraints. VERTICAL和GridBagConstraints.BOTH.。
如果组件没有填充整个区域,可以通过设置anchor域指定其位置。有效值为GridBag Constraints.CENTER(默认值)、GridBagConstraints.NORTH、GridBagConstraints.NORTHEAST和 GridBagConstraints.EAST等。 -
填充
可以通过设置GridBagLayout的 insets域在组件周围增加附加的空白区域。通过设置Insets对象的left , top , right和 bottom指定组件周围的空间量。这被称作外部填充(或外边距)(external padding)。
通过设置 ipadx 和 ipady指定内部填充(或内外距)(internal padding)。这两个值被加到组件的最小宽度和最小高度上。这样可以保证组件不会收缩至最小尺寸之下。 -
指定gridx、gridy、gridwidth、gridheight参数的另一种方法
AWT文档建议不要将gridx和gridy设置为绝对位置,应该将它们设置为常量GridBagConstraints.RELATIVE。然后,按照标准的顺序,将组件添加到网格组布局中。即第一行从左向右,然后再开始新的一行,以此类推。
还需要通过为gridheight和 gridwidth域指定一个适当的值来设置组件横跨的行数和列数。除此之外,如果组件扩展至最后一行或最后一列,则不要给出一个实际的数值,而是用常量GridBagConstraints.REMAINDER替代,这样会告诉布局管理器这个组件是本行上的最后一个组件。
网格组布局使用的策略:- 在纸上画出组件布局草图。
- 找出一种网格,小组件被放置在一个单元格内,大组件将横跨多个单元格。
- 用0,1,2……标识网格的行和列。现在可以读取gridx, gridy, gridwidth和 gridheight的值。
- 对于每个组件,需要考虑下列问题:
- 是否需要水平或者垂直填充它所在的单元格?
- 如果不需要,希望如何排列?
这些就是fill和 anchor参数的设置。
- 将所有的增量设置为100。如果需要某行或某列始终保持默认的大小,就将这行或这列中所有组件的weightx和 weighty设置为0。
- 编写代码。仔细地检查GridBagConstraints 的设置。错误的约束可能会破坏整个布局。
- 编译、运行。
-
使用帮助类来管理网格组约束
网格组布局最乏味的工作就是为设置约束编写代码。为此,很多程序员编写帮助函数或者帮助类来满足上面的目的。下面是为字体对话框示例编写的帮助类。这个类有下列特性:
-
名字简短:GBC代替GridBagConstraints。
-
扩展于GridBagConstraints,因此可以使用约束的缩写,如GBC.EAST。
-
当添加组件时,使用GBC对象,如:
add(component,new GBC(1,2));
-
有两个构造器可以用来设置最常用的参数:gridx和 gridy,或者gridx、gridy,gridwidth 和 gridheight。
add(component,new CBC(1,2,1,4)); -
域有很便捷的设置方法,采用x/y值对形式:
add(component,new CBC(1,2).setweight(100,100));
-
设置方法将返回this,所以可以链接它们:
add(component,new GBC(1,2).setAnchor(GBC.EAST).setweight(100,100)); -
setInsets方法将构造Inset对象。要想获取1个像素的insets,可以调用:
add(component,new GBC(1,2) .setAnchor(GBC.EAST).setInsets(1));
-
package Test.Swing.gridbag;
import java.awt.event.ActionListener;
import java.awt.Font;
import java.awt.GridBagLayout;
import javax.swing.BorderFactory;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextArea;
public class FontFrameTest {
public static void main(String[] args) {
FontFrame fontFrame = new FontFrame();
fontFrame.setVisible(true);
}
}
class FontFrame extends JFrame {
public static final int TEXT_ROWS = 10;
public static final int TEXT_COLS = 20;
private JComboBox<String> faceComboBox;
private JComboBox<Integer> size;
private JCheckBox bold;
private JCheckBox italic;
private JTextArea sample;
private JLabel faceLabel;
private JLabel sizeLabel;
private void Init() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
GridBagLayout layout = new GridBagLayout();
setLayout(layout);
faceComboBox = new JComboBox<>(new String[] { "Serif", "Sanserif", "Monospace", "Dialog", "DialogInput" });
size = new JComboBox<>(new Integer[] { 8, 10, 12, 14, 20, 36, 48 });
bold = new JCheckBox("Bold");
italic = new JCheckBox("Italic");
sample = new JTextArea(TEXT_ROWS, TEXT_COLS);
sample.setText("The Quick Fox Jumps Over The Lazy Dog !");
sample.setEditable(false);
sample.setLineWrap(true);
sample.setBorder(BorderFactory.createEtchedBorder());
faceLabel = new JLabel("Face: ");
sizeLabel = new JLabel("Size: ");
}
public FontFrame() {
Init();
ActionListener listener = event -> updateSample();
faceComboBox.addActionListener(listener);
size.addActionListener(listener);
bold.addActionListener(listener);
italic.addActionListener(listener);
add(faceLabel, new GBC(0, 0).setAnchor(GBC.EAST));
add(faceComboBox, new GBC(1, 0).setFill(GBC.HORIZONTAL).setWeight(100, 0).setInsets(1));
add(sizeLabel, new GBC(0, 1).setAnchor(GBC.EAST));
add(size, new GBC(1, 1).setFill(GBC.HORIZONTAL).setWeight(100, 0).setInsets(1));
add(bold, new GBC(0, 2, 2, 1).setAnchor(GBC.CENTER).setWeight(100, 100));
add(italic, new GBC(0, 3, 2, 1).setAnchor(GBC.CENTER).setWeight(100, 100));
add(sample, new GBC(2, 0, 1, 4).setFill(GBC.BOTH).setWeight(100, 100));
pack();
updateSample();
}
private void updateSample() {
String fontFace = (String) faceComboBox.getSelectedItem();
int fontStyle = (bold.isSelected() ? Font.BOLD : 0) + (italic.isSelected() ? Font.ITALIC : 0);
int fontSize = size.getItemAt(size.getSelectedIndex());
Font font = new Font(fontFace, fontStyle, fontSize);
sample.setFont(font);
sample.repaint();
}
}
package Test.Swing.gridbag;
import java.awt.GridBagConstraints;
import java.awt.Insets;
public class GBC extends GridBagConstraints {
/**
* Constructs a GBC with a given gridx and gridy position and all other
* grid bag constraint values set to default.
*
* @param gridx the gridx position
* @param gridy the gridy position
*/
public GBC(int gridx, int gridy) {
this.gridx = gridx;
this.gridy = gridy;
}
/**
* Constructs a GBC with a given gridx , gridy, gridwidth and
* gridheight and all other grid bag constraint values set to default.
*
* @param gridx the gridx position
* @param gridy the gridy position
* @param gridwidth the cell span in x-direction
* @param gridheight the cell span in y-direction
*/
public GBC(int gridx, int gridy, int gridwidth, int gridheight) {
this.gridx = gridx;
this.gridy = gridy;
this.gridwidth = gridwidth;
this.gridheight = gridheight;
}
/**
* Sets the anchor
*
* @param anchor the anchor value
* @return this object for further modification
*/
public GBC setAnchor(int anchor) {
this.anchor = anchor;
return this;
}
/**
* Sets the fill direction
*
* @param fill thi fill direction
* @return this object for further modification
*/
public GBC setFill(int fill) {
this.fill = fill;
return this;
}
/**
* Sets the cell weight.
*
* @param weightx the cell weight in x-direction
* @param weighty the cell weight in y-direction
* @return this object for further modification
*/
public GBC setWeight(double weightx, double weighty) {
this.weightx = weightx;
this.weighty = weighty;
return this;
}
/**
* Sets the insets of the cell
*
* @param distance the spacing yo use in all directions
* @return this object for further modification
*/
public GBC setInsets(int distance) {
this.insets = new Insets(distance, distance, distance, distance);
return this;
}
/**
* Set top, left, bottom, and right to the specified values
*
* @param top the inset from the top.
* @param left the inset from the left.
* @param bottom the inset from the bottom.
* @param right the inset from the right.
* @return this object for further modification
*/
public GBC setInsets(int top, int left, int bottom, int right) {
this.insets = new Insets(top, left, bottom, right);
return this;
}
/**
* Sets the internal padding
*
* @param ipadx the internal padding in x-direction
* @param ipady the internal padding in y-direction
* @return this object for further modification
*/
public GBC setIpad(int ipadx, int ipady) {
this.ipadx = ipadx;
this.ipady = ipady;
return this;
}
}
-
组布局(GroupLayout)
-
不使用布局管理器
有时候用户可能不想使用任何布局管理器,而只想把组件放在一个固定的位置上(通常称为绝对定位)。这对于与平台无关的应用程序来说并不是一个好主意,但可用来快速地构造原型。
下面是将一个组件定位到某个绝对定位的步骤:- 将布局管理器设置为null。
- 将组件添加到容器中。
- 指定想要放置的位置和大小。
frame.setLayout(nul1);
JButton ok = new JButton(“OK”");
frame.add(ok);
ok.setBounds(10,10,30,15);
-
定制布局管理器
原则上,可以通过自己设计LayoutManager类来实现特殊的布局方式。
定制布局管理器必须实现LayoutManager接口,并且需要覆盖下面5个方法:
void addLayoutComponent(String s,Component c);void removeLayoutComponent(Component c);
Dimension preferredLayoutsize(Container parent);Dimension minimumLayoutSize(Container parent);void 1ayoutContainer(Container parent);
在添加或删除一个组件时会调用前面两个方法。如果不需要保存组件的任何附加信息,那么可以让这两个方法什么都不做。接下来的两个方法计算组件的最小布局和首选布局所需要的空间。两者通常相等。第5个方法真正地实施操作,它调用所有组件的 setBounds方法。 -
遍历顺序
遍历顺序很直观,它的顺序是从左至右,从上至下。
如果容器还包含其他的容器,情况就更加复杂了。当焦点给予另外一个容器时,那个容器左上角的组件就会自动地获得焦点,然后再遍历那个容器中的所有组件。最后,将焦点移交给紧跟着那个容器的组件。
7. 对话框
到目前为止,所有的用户界面组件都显示在应用程序创建的框架窗口中。这对于编写运行在 Web浏览器中的 applets来说是十分常见的情况。但是,如果编写应用程序,通常就需要弹出独立的对话框来显示信息或者获取用户信息。
与大多数的窗口系统一样,AWT也分为模式对话框
和无模式对话框
。所谓模式对话框是指在结束对它的处理之前,不允许用户与应用程序的其余窗口进行交互。模式对话框主要用于在程序继续运行之前获取用户提供的信息。例如,当用户想要读取文件时,就会弹出一个模式对话框。用户必须给定一个文件名,然后程序才能够开始读操作。只有用户关闭(模式)对话框之后,应用程序才能够继续执行。
所谓无模式对话框是指允许用户同时在对话框和应用程序的其他窗口中输入信息。使用无模式对话框的最好例子就是工具栏。工具栏可以停靠在任何地方,并且用户可以在需要的时候,同时与应用程序窗口和工具栏进行交互。
-
选项对话框
Swing 有一套简单的对话框,用于获取用户的一些简单信息。JOptionPane有4个用于显示这些对话框的静态方法:
showMessageDialog: 显示一条消息并等待用户点击OK
showConfirmDialog: 显示一条消息并等待用户确认(与OK/Cancel类似)
showOptionDialog: 显示一条消息并获得用户在一组选项中的选择
showInputDialog: 显示一条消息并获得用户输入的一行文本输入对话框有一个用于接收用户输入的额外组件。它既可能是用于输入任何字符串的文本域,也可能是允许用户从中选择的组合框。
左侧的图标将由下面5种消息类型决定:
ERROR_MESSAGE
INFORMATION_MESSAGE
WARNING_MESSAGE
QUESTION_MESSAGE
PLAIN_MESSAGE
PLAIN_MESSAGE类型没有图标。每个对话框类型都有一个方法,可以用来提供自己的图标,以替代原来的图标。
可以为每个对话框类型指定一条消息。这里的消息既可以是字符串、图标、用户界面组件,也可以是其他类型的对象。下面是显示消息对象的基本方式:
String: 绘制字符串
Icon: 显示图标
Component: 显示组件
Object[]: 显示数组中的所有对象,依次叠加
任何其他对象: 调用toString方法来显示结果字符串当然,提供字符串消息是最常见的情况,而提供一个Component 会带来更大的灵活性。这是因为通过调用paintComponent方法可以绘制自己想要的任何内容。
位于底部的按钮取决于对话框类型和选项类型。当调用showMessageDialog和showImputDialog时,只能看到一组标准按钮(分别是OK/Cancel)。当调用showConfirmDialog时,可以选择下面四种选项类型之一:
DEFAULT_OPTION
YES NO_OPTION
YES_NO_CANCEL_OPTION
OK_CANCEL_OPTION
使用showOptionDialog可以指定任意的选项。这里需要为选项提供一个对象数组。每个数组元素可以是下列类型之一:
String: 使用字符串标签创建一个按钮
Icon: 使用图标创建一个按钮
Component: 显示这个组件
其他类型的对象: 使用toString方法,然后用结果字符串作为标签创建按钮
下面是这些方法的返回值:
showMessageDialog 无
showConfirmDialog 表示被选项的一个整数
showOptionDialog 表示被选项的一个整数
showInputDialog 用户选择或输入的字符串
showConfirmDialog 和 showOptionDialog返回一个整数用来表示用户选择了哪个按钮。对于选项对话框来说,这个值就是被选的选项的索引值或者是CLOSED_OPTION(此时用户没有选择可选项,而是关闭了对话框)。对于确认对话框,返回值可以是下列值之一:
OK_OPTION
CANCEL_OPTION
YES_OPTION
NO_OPTION
CLOSED_OPTION这些选项似乎令人感到迷惑不解,实际上非常简单步骤如下:
- 选择对话框的类型(消息、确认、选项或者输入)。
- 选择图标(错误、信息、警告、问题、无或者自定义)。
- 选择消息(字符串、图表、自定义组件或者它们的集合)。
- 对于确认对话框,选择选项类型(默认、Yes/No、Yes/No/Cancel或者Ok/Cancel)。
- 对于选项对话框,选择选项(字符串、图表或者自定义组件)和默认选项。
- 对于输入对话框,选择文本框或者组合框。
- 调用JOptionPane API中的相应方法。
package Test.Swing.optionDialog;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Rectangle2D;
import java.util.Date;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import Test.ChangeDefaultUI;
public class OptionDialogFrame extends JFrame {
private final String messageString = "Message";
private final Icon messageIcon = new ImageIcon("D:\\VSCode\\images\\qunmap-center-icon.png");
private final Object messageObject = new Date();
private final Component messageComponent = new SampleComponent();
private ButtonPanel typePanel;
private ButtonPanel messagePanel;
private ButtonPanel messageTypePanel;
private ButtonPanel optionTypePanel;
private ButtonPanel optionsPanel;
private ButtonPanel inputPanel;
public OptionDialogFrame() {
Init();
JPanel gridPanel = new JPanel();
gridPanel.setLayout(new GridLayout(2, 2));
gridPanel.add(typePanel);
gridPanel.add(messageTypePanel);
gridPanel.add(messagePanel);
gridPanel.add(optionTypePanel);
gridPanel.add(optionsPanel);
gridPanel.add(inputPanel);
JPanel showPanel = new JPanel();
JButton showButton = new JButton("show");
showButton.addActionListener(new showAction());
showPanel.add(showButton);
add(gridPanel, BorderLayout.CENTER);
add(showPanel, BorderLayout.SOUTH);
pack();
}
private void Init() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
typePanel = new ButtonPanel("Type", "Message", "Confirm", "Option", "Input");
messageTypePanel = new ButtonPanel("Message Type", "ERROR_MESSAGE", "INFORMATION_MESSAGE", "WARNING_MESSAGE",
"QUESTION_MESSAGE", "PLAIN_MESSAGE");
messagePanel = new ButtonPanel("Message", "String", "Icon", "Component", "Other", "Object[]");
optionTypePanel = new ButtonPanel("Confirm", "DEFAULT_OPTION", "YES_NO_OPTION", "YES_NO_CANCEL_OPTION",
"OK_CANCEL_OPTION");
optionsPanel = new ButtonPanel("Option", "String[]", "Icon[]", "Object[]");
inputPanel = new ButtonPanel("Input", "Text Field", "Combo box");
}
/**
* Gets the currently selected message.
*
* @return a string, icon,component, or object array, depending ont the Message
* panel selection.
*/
public Object getMessageObject() {
String message = messagePanel.getSelection();
switch (message) {
case "String":
return messageString;
case "Icon":
return messageIcon;
case "Component":
return messageComponent;
case "Object[]":
return new Object[] { messageString, messageIcon, messageComponent, messageObject };
case "Other":
return messageObject;
default:
return null;
}
}
/**
* Gets the selected message or option type.
*
* @param panel the Message Type or Confirm panel.
* @return the Selected XXX_MESSAGE or XXX_OPTION constant from the JOptionPane
* class
*/
public int getType(ButtonPanel panel) {
String message = panel.getSelection();
try {
return JOptionPane.class.getField(message).getInt(null);
} catch (IllegalAccessException | NoSuchFieldException e) {
return -1;
}
}
/**
* Gets the currently selected options.
*
* @return an array of string, icon, object depending ont the Option panel
* selection.
*/
private Object[] getOptions() {
String message = optionsPanel.getSelection();
switch (message) {
case "String[]":
return new String[] { "Yellow", "Blue", "Red" };
case "Icon[]":
return new Icon[] { new ImageIcon("D:\\VSCode\\images\\qunmap-center-icon.png") };
case "Object[]":
return new Object[] { messageString, messageIcon, messageComponent, messageObject };
default:
return null;
}
}
private class showAction implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
String type = typePanel.getSelection();
switch (type) {
case "Confirm":
JOptionPane.showConfirmDialog(OptionDialogFrame.this, getMessageObject(), "Title",
getType(optionTypePanel), getType(messageTypePanel));
break;
case "Input":
if (inputPanel.getSelection().equals("Text Field"))
JOptionPane.showInputDialog(OptionDialogFrame.this, getMessageObject(), "Title",
getType(messageTypePanel));
else
JOptionPane.showInputDialog(OptionDialogFrame.this, getMessageObject(), "Title",
getType(messageTypePanel), null, new String[] { "Yellow", "Blue", "Red" }, "Blue");
break;
case "Message":
JOptionPane.showMessageDialog(OptionDialogFrame.this, getMessageObject(), "Title",
getType(messageTypePanel));
break;
case "Option":
JOptionPane.showOptionDialog(OptionDialogFrame.this, getMessageObject(), "Title",
getType(optionTypePanel), getType(messageTypePanel), null, getOptions(), getOptions()[0]);
break;
default:
break;
}
}
}
public static void main(String[] args) {
ChangeDefaultUI.changeDefaultUI();
OptionDialogFrame optionDialogFrame = new OptionDialogFrame();
optionDialogFrame.setVisible(true);
}
}
class SampleComponent extends JComponent {
@Override
protected void paintComponent(Graphics g) {
Graphics2D graphics2D = (Graphics2D) g;
Rectangle2D rectangle2D = new Rectangle2D.Double(0, 0, getWidth() - 1, getHeight() - 1);
graphics2D.setPaint(Color.YELLOW);
graphics2D.fill(rectangle2D);
graphics2D.setPaint(Color.BLUE);
graphics2D.draw(rectangle2D);
}
public Dimension getPreferredDimension() {
return new Dimension(10, 10);
}
}
package Test.Swing.optionDialog;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
public class ButtonPanel extends JPanel {
private final ButtonGroup group;
public ButtonPanel(String title, String... options) {
setBorder(BorderFactory.createTitledBorder(BorderFactory.createEmptyBorder(), title));
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
group = new ButtonGroup();
for (String option : options) {
JRadioButton button = new JRadioButton(option);
button.setActionCommand(option);
add(button);
group.add(button);
button.setSelected(option.equals(options[0]));
}
}
public String getSelection() {
return group.getSelection().getActionCommand();
}
}
-
创建对话框
要想实现一个对话框,需要从JDialog 派生一个类。这与应用程序窗口派生于JFrame的过程完全一样。具体过程如下:- 在对话框构造器中,调用超类JDialog的构造器。
- 添加对话框的用户界面组件。
- 添加事件处理器。
- 设置对话框的大小。
在调用超类构造器时,需要提供拥有者框架(owner frame)、对话框标题及模式特征。拥有者框架控制对话框的显示位置,如果将拥有者标识为null,那么对话框将由一个隐藏框架所拥有。
模式特征将指定对话框处于显示状态时,应用程序中其他窗口是否被锁住。无模式对话框不会锁住其他窗口,而有模式对话框将锁住应用程序中的所有其他窗口(除对话框的子窗口外)。用户经常使用的工具栏就是无模式对话框,另一方面,如果想强迫用户在继续操作之前提供一些必要的信息就应该使用模式对话框。
-
数据交换
package Test.Swing.dataExchange;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Frame;
import java.awt.GridLayout;
import java.util.Arrays;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
public class PassworChooser extends JPanel {
private JTextField userName;
private JPasswordField password;
private JButton okButton;
private boolean ok;
private JDialog dialog;
public PassworChooser() {
setLayout(new BorderLayout());
JPanel panel = new JPanel();
panel.setLayout(new GridLayout(2, 2));
panel.add(new JLabel("User Name: "));
panel.add(userName = new JTextField(""));
panel.add(new JLabel("Password: "));
panel.add(password = new JPasswordField(""));
add(panel, BorderLayout.CENTER);
JButton cancelButton = new JButton("Cancel");
cancelButton.addActionListener(event -> dialog.setVisible(false));
JPanel butJPanel = new JPanel();
okButton = new JButton("OK");
okButton.addActionListener(event -> {
ok = true;
dialog.setVisible(true);
});
butJPanel.add(okButton);
butJPanel.add(cancelButton);
add(butJPanel, BorderLayout.SOUTH);
}
public void setUser(User user) {
userName.setText(user.getUserName());
}
public User getUser() {
return new User(userName.getText(), String.copyValueOf(password.getPassword()));
}
public boolean showDialog(Component parent, String title) {
ok = false;
Frame owner = null;
if (parent instanceof Frame) {
owner = (Frame) parent;
} else {
owner = (Frame) SwingUtilities.getAncestorOfClass(Frame.class, parent);
}
if (dialog == null || dialog.getOwner() != owner) {
dialog = new JDialog(owner, true);
dialog.add(this);
dialog.getRootPane().setDefaultButton(okButton);
dialog.pack();
}
dialog.setTitle(title);
dialog.setVisible(true);
return ok;
}
}
package Test.Swing.dataExchange;
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import Test.ChangeDefaultUI;
public class DataExchangeFrame extends JFrame {
public final int TEXT_ROWS = 20;
public final int TEXT_COLS = 40;
private PassworChooser dialog;
private JTextArea textArea;
public DataExchangeFrame() {
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
JMenu fileMenu = new JMenu("File");
menuBar.add(fileMenu);
JMenuItem connectItem = new JMenuItem("Connect");
connectItem.addActionListener(new ConnectAction());
fileMenu.add(connectItem);
fileMenu.addSeparator();
JMenuItem exitItem = new JMenuItem("Exit");
exitItem.addActionListener(event -> System.exit(0));
fileMenu.add(exitItem);
textArea = new JTextArea(TEXT_ROWS, TEXT_COLS);
add(new JScrollPane(textArea), BorderLayout.CENTER);
pack();
}
private class ConnectAction implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
if (dialog == null)
dialog = new PassworChooser();
dialog.setUser(new User("yourname", null));
if (dialog.showDialog(DataExchangeFrame.this, "Content")) {
User u = dialog.getUser();
textArea.append(
"User Name = " + u.getUserName() + ", Password = " + u.getPassword() + "\n");
}
}
}
public static void main(String[] args) {
ChangeDefaultUI.changeDefaultUI();
DataExchangeFrame dataExchangeFrame = new DataExchangeFrame();
dataExchangeFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
dataExchangeFrame.setVisible(true);
}
}
-
文件对话框
下面是建立文件对话框并且获取用户选择信息的步骤:-
建立一个JFileChooser对象。与JDialog类的构造器不同,它不需要指定父组件。允许在多个框架中重用一个文件选择器。例如:
JFi1eChooser chooser = new JFileChooser(; -
2)调用setCurrentDirectory方法设置当前目录。例如,使用当前的工作目录:
chooser.setCurrentDirectory(new File(" .")); -
如果有一个想要作为用户选择的默认文件名,可以使用setSelectedFile方法进行指定:
chooser.setSelectedFile(new File(filename)); -
如果允许用户在对话框中选择多个文件,需要调用setMultiSelectionEnabled方法。当然,这是可选的。
chooser.setMultiSelectionEnabled(true); -
如果想让对话框仅显示某一种类型的文件(如,所有扩展名为.gif的文件),需要设置文件过滤器,稍后将会进行讨论。
-
在默认情况下,用户在文件选择器中只能选择文件。如果希望选择目录,需要调用setFileSelectionMode方法。参数值为:
JFileChooser.FILES_ONLY(默认值),
JFileChooser.DIRECTORIES_ONLY或者
JFileChooser.FILES_AND_DIRECTORIES。 -
调用showOpenDialog或者showSaveDialog 方法显示对话框。必须为这些调用提供父组件:
int result = chooser.showOpenDialog(parent);
或者
int result = chooser.showSaveDialog(parent);
这些调用的区别是“确认按钮”的标签不同。
仅当用户确认、取消或者离开对话框时才返回调用。返回值可以是JFileChooser.APPROVE_OPTION、JFileChooser.CANCEL_OPTION或者JFileChooser.ERROR_OPTION. -
调用getSelectedFile()或者getSelectedFiles()方法获取用户选择的一个或多个文件。
若想限制显示的文件,需要创建一个实现了抽象类javax.swing.filechooser.FileFilter的对象。文件选择器将每个文件传递给文件过滤器,只有文件过滤器接受的文件才被最终显示出来。
有两个子类可用:可以接受所有文件的默认过滤器和可以接受给定扩展名的所有文件的过滤器。其实,设计专用文件过滤器非常简单,只要实现FileFilter超类中的两个方法即可:
public boolean accept(File f);
public String getDescription(;
第一个方法检测是否应该接受一个文件,第二个方法返回显示在文件选择器对话框中显示的文件类型的描述信息。一旦有了文件过滤器对象,就可以调用JFileChooser类中的setFileFilter方法,将这个对象安装到文件选择器对象中:
chooser.setFileFilter(new FileNameExtensionFilter(“Image files”,“gif”,“jp9”));
可以为一个文件选择器安装多个过滤器:
chooser.addChoosableFileFilter(fi1ter1);
chooser.addChoosableFileFilter(fi1ter2);
. . . -
-
颜色选择器
下面这段代码说明了如何利用颜色选择器显示模式对话框:
Color selectedColor = JColorChooser.showDialog(parent,title,initialColor);
另外,也可以显示无模式颜色选择器对话框,需要提供:- 一个父组件。
- 对话框的标题。
- 选择模式/无模式对话框的标志。
- 颜色选择器。
- OK和 Cancel按钮的监听器(如果不需要监听器可以设置为null)。
8. GUI程序排错
-
调试技巧
如果你看过Swing窗口,肯定想知道它的设计者如何把组件摆放得如此恰到好处,可以查看它的内容。按下Ctrl+Shift+F1得到所有组件的层次结构输出。javax.swing.JFrame[frame0,0,0,300x200,layout=java.awt.BorderLayout,title=,resizable,normal,defaultCloseOperation=EXIT_ON_CLOSE,rootPane=javax.swing.JRootPane[,9,38,282x153,layout=javax.swing.JRootPane R o o t L a y o u t , a l i g n m e n t X = 0.0 , a l i g n m e n t Y = 0.0 , b o r d e r = j a v a x . s w i n g . p l a f . s y n t h . S y n t h B o r d e r @ f 73394 , f l a g s = 16777673 , m a x i m u m S i z e = , m i n i m u m S i z e = , p r e f e r r e d S i z e = ] , r o o t P a n e C h e c k i n g E n a b l e d = t r u e ] j a v a x . s w i n g . J R o o t P a n e [ , 9 , 38 , 282 x 153 , l a y o u t = j a v a x . s w i n g . J R o o t P a n e RootLayout,alignmentX=0.0,alignmentY=0.0,border=javax.swing.plaf.synth.SynthBorder@f73394,flags=16777673,maximumSize=,minimumSize=,preferredSize=],rootPaneCheckingEnabled=true] javax.swing.JRootPane[,9,38,282x153,layout=javax.swing.JRootPane RootLayout,alignmentX=0.0,alignmentY=0.0,border=javax.swing.plaf.synth.SynthBorder@f73394,flags=16777673,maximumSize=,minimumSize=,preferredSize=],rootPaneCheckingEnabled=true]javax.swing.JRootPane[,9,38,282x153,layout=javax.swing.JRootPaneRootLayout,alignmentX=0.0,alignmentY=0.0,border=javax.swing.plaf.synth.SynthBorder@f73394,flags=16777673,maximumSize=,minimumSize=,preferredSize=]
javax.swing.JPanel[null.glassPane,0,0,282x153,hidden,layout=java.awt.FlowLayout,alignmentX=0.0,alignmentY=0.0,border=javax.swing.plaf.synth.SynthBorder@758a72,flags=16777217,maximumSize=,minimumSize=,preferredSize=]
javax.swing.JLayeredPane[null.layeredPane,0,0,282x153,alignmentX=0.0,alignmentY=0.0,border=,flags=0,maximumSize=,minimumSize=,preferredSize=,optimizedDrawingPossible=true]
javax.swing.JPanel[null.contentPane,0,0,282x153,layout=javax.swing.JRootPane 1 , a l i g n m e n t X = 0.0 , a l i g n m e n t Y = 0.0 , b o r d e r = j a v a x . s w i n g . p l a f . s y n t h . S y n t h B o r d e r @ d e 07 f 8 , f l a g s = 9 , m a x i m u m S i z e = , m i n i m u m S i z e = , p r e f e r r e d S i z e = ] j a v a x . s w i n g . J P a n e l [ , 0 , 0 , 282 x 153 , l a y o u t = j a v a . a w t . G r i d L a y o u t , a l i g n m e n t X = 0.0 , a l i g n m e n t Y = 0.0 , b o r d e r = j a v a x . s w i n g . p l a f . s y n t h . S y n t h B o r d e r @ c 7 a 574 , f l a g s = 9 , m a x i m u m S i z e = , m i n i m u m S i z e = , p r e f e r r e d S i z e = ] j a v a x . s w i n g . J P a s s w o r d F i e l d [ , 0 , 0 , 282 x 76 , l a y o u t = j a v a x . s w i n g . p l a f . b a s i c . B a s i c T e x t U I 1,alignmentX=0.0,alignmentY=0.0,border=javax.swing.plaf.synth.SynthBorder@de07f8,flags=9,maximumSize=,minimumSize=,preferredSize=] javax.swing.JPanel[,0,0,282x153,layout=java.awt.GridLayout,alignmentX=0.0,alignmentY=0.0,border=javax.swing.plaf.synth.SynthBorder@c7a574,flags=9,maximumSize=,minimumSize=,preferredSize=] javax.swing.JPasswordField[,0,0,282x76,layout=javax.swing.plaf.basic.BasicTextUI 1,alignmentX=0.0,alignmentY=0.0,border=javax.swing.plaf.synth.SynthBorder@de07f8,flags=9,maximumSize=,minimumSize=,preferredSize=]javax.swing.JPanel[,0,0,282x153,layout=java.awt.GridLayout,alignmentX=0.0,alignmentY=0.0,border=javax.swing.plaf.synth.SynthBorder@c7a574,flags=9,maximumSize=,minimumSize=,preferredSize=]javax.swing.JPasswordField[,0,0,282x76,layout=javax.swing.plaf.basic.BasicTextUIUpdateHandler,alignmentX=0.0,alignmentY=0.0,border=javax.swing.plaf.synth.SynthBorder@1ef436b,flags=288,maximumSize=,minimumSize=,preferredSize=,caretColor=,disabledTextColor=DerivedColor(color=142,143,145 parent=nimbusDisabledText offsets=0.0,0.0,0.0,0 pColor=142,143,145,editable=true,margin=javax.swing.plaf.InsetsUIResource[top=0,left=0,bottom=0,right=0],selectedTextColor=DerivedColor(color=255,255,255 parent=nimbusSelectedText offsets=0.0,0.0,0.0,0 pColor=255,255,255,selectionColor=DerivedColor(color=57,105,138 parent=nimbusSelectionBackground offsets=0.0,0.0,0.0,0 pColor=57,105,138,columns=30,columnWidth=0,command=,horizontalAlignment=LEADING,echoChar=*]
javax.swing.JButton[,0,76,282x76,alignmentX=0.0,alignmentY=0.5,border=javax.swing.plaf.synth.SynthBorder@11d697e,flags=288,maximumSize=,minimumSize=,preferredSize=,defaultIcon=,disabledIcon=,disabledSelectedIcon=,margin=javax.swing.plaf.InsetsUIResource[top=0,left=0,bottom=0,right=0],paintBorder=true,paintFocus=true,pressedIcon=,rolloverEnabled=true,rolloverIcon=,rolloverSelectedIcon=,selectedIcon=,text=OK,defaultCapable=true]可以使用JComponent类的setDebugGraphicsOptions方法打开寸一个Swing 组件的调试。有以下几个选项
DebugGraphics.FLASH_OPTION | 绘制前用红色闪烁的显示各条线、矩形和文本 |
---|---|
DebugGraphics.LOG_OPTION | 为每一个绘制操作打印一个消息 |
DebugGraphics.BUFFERRED_OPTION | 显示在离屏缓冲区完成的操作 |
DebugGraphics.NONE_OPTION | 关闭图形调试 |
我们发现,要让闪烁选项起作用,必须禁用“双缓冲”——这是Swing更新窗口时为减少闪烁所用的策略。打开闪烁选项的魔咒是:
RepaintManager.currentManager(getRootPane()).setDoubleBufferingEnabled(false);
((JComponent) getContentPane()).setDebugGraphicsOptions(DebugGraphics.FLASH_OPTION);
只需要把这些代码行放在 frame窗口构造器的末尾。程序运行时,你将看到会用慢动作填内容窗格。或者,对于更本地化的调试,只需要为组件调用setDebugGraphicsOptions.
-
让AWT机器人完成工作
Robot类可以向任何AWT程序发送按键和鼠标点击事件。这个类就是用来自动测试用户界面的。
要得到一个机器人,首先需要得到一个GraphicsDevice对象。可以通过以下调用序列得到默认的屏幕设备:
GraphicsEnvironment environment = GraphicsEnvironment.getLocalCraphicsEnvironment();GraphicsDevice screen = environment.getDefaultscreenDevice();
然后构造一个机器人:
Robot robot = new Robot(screen);
若要发送一个按键事件,需告知机器人模拟按下和松开按键:
robot.keyPress(KeyEvent.VK_TAB);
robot.keyRelease(KeyEvent.VK_TAB);
对于鼠标点击事件,首先需要移动鼠标,然后按下再释放鼠标按钮:
robot.mouseMove(x,y);// x and y are absolute screen pixel coordinates.
robot.mousePress(InputEvent.BUTTON1_MASK);
robot.mouseRelease(InputEvent.BUTTON1_MASK);我们的思路是首先模拟按键和鼠标输人,然后截屏来查看应用是否完成了它该完成的工.乍。截屏需要使用createScreenCapture方法:
Rectangle rect = new Rectangle(x,y, width,height);BufferedImage image = robot.createScreenCapture(rect);
矩阵坐标也指示绝对屏幕像素。
最后,通常我们都希望在机器人指令之间增加一个很小的延迟,使应用能跟得上。可以吏用delay方法并提供延迟时间(毫秒数)。例如:
robot.delay(1000);// delay by 1000 milliseconds
package com.company.Tools;
import com.company.ChangeDefaultUI;
import javax.swing.*;
import javax.swing.UIManager.LookAndFeelInfo;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
public class RobotTest {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
ChangeDefaultUI.changeDefaultUI();
ButtonFrame buttonFrame = new ButtonFrame();
buttonFrame.setTitle("ButtonTest");
buttonFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
buttonFrame.setVisible(true);
});
GraphicsEnvironment environment = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice screen = environment.getDefaultScreenDevice();
try {
final Robot robot = new Robot(screen);
robot.waitForIdle();
new Thread() {
public void run() {
runTest(robot);
}
}.start();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void runTest(Robot robot) {
robot.delay(2000);
robot.keyPress(' ');
robot.keyRelease(' ');
robot.delay(4000);
robot.keyPress(KeyEvent.VK_TAB);
robot.keyRelease(KeyEvent.VK_TAB);
robot.keyPress(' ');
robot.keyRelease(' ');
robot.delay(4000);
robot.mouseMove(220, 40);
robot.mousePress(InputEvent.BUTTON1_MASK);
robot.mouseRelease(InputEvent.BUTTON1_MASK);
robot.delay(4000);
BufferedImage image = robot.createScreenCapture(new Rectangle(0, 0, 400, 300));
ImageFrame frame = new ImageFrame(image);
frame.setVisible(true);
}
}
class ButtonFrame extends JFrame {
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 200;
private final JPanel buttonPanel;
public ButtonFrame() {
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
JButton yellowButton = new JButton("Yellow");
JButton blueButton = new JButton("Blue");
JButton redButton = new JButton("Red");
buttonPanel = new JPanel();
buttonPanel.add(yellowButton);
buttonPanel.add(blueButton);
buttonPanel.add(redButton);
ColorAction yellowAction = new ColorAction(Color.YELLOW);
ColorAction blueAction = new ColorAction(Color.BLUE);
ColorAction redAction = new ColorAction(Color.RED);
yellowButton.addActionListener(yellowAction);
blueButton.addActionListener(blueAction);
redButton.addActionListener(redAction);
add(buttonPanel);
}
public void changeLookAndFeel() {
String className = "javax.swing.plaf.nimbus.NimbusLookAndFeel";
try {
UIManager.LookAndFeelInfo[] infos = UIManager.getInstalledLookAndFeels();
for (LookAndFeelInfo lookAndFeelInfo : infos) {
System.out.println(lookAndFeelInfo.toString());
}
UIManager.setLookAndFeel(className);
SwingUtilities.updateComponentTreeUI(this);
pack();
} catch (Exception e) {
e.printStackTrace();
}
}
private class ColorAction implements ActionListener {
private final Color backgroundColor;
public ColorAction(Color color) {
backgroundColor = color;
}
@Override
public void actionPerformed(ActionEvent e) {
buttonPanel.setBackground(backgroundColor);
}
}
}
class ImageFrame extends JFrame {
public ImageFrame(Image image) {
setTitle("Capture");
setSize(450, 350);
JLabel label = new JLabel(new ImageIcon(image));
add(label);
}
}
十三、部署Java应用程序
1. JAR文件
-
创建jar文件
可以使用jar 工具制作JAR文件(在默认的JDK安装中,位于jdk/bin目录下)。创建一个新的JAR文件应该使用的常见命令格式为:
jar cvf JARFileName File1 File2 …jar可选项:
jar命令选项
x0选项 | 说明 |
---|---|
c | 创建一个新的或者空的存档文件并加入文件,如果指定的文件名是亩,jar程序或进行递归处理 |
C | 暂时改变目录 |
e | 在清单文件中新建一个条目 |
f | 将jar文件名指定为第二个命令参数 |
i | 建立索引文件 |
m | 将一个清单文件添加到JAR文件中(清单文件时对存档内容的说明) |
M | 不创建清单文件 |
t | 显示内容表 |
u | 更新一个已有的jar文件 |
v | 生成详细的输出结果 |
解压文件 | |
存储,不进行ZIP压缩 |
-
清单文件
除了类文件、图像和其他资源外,每个JAR文件还包含一个用于描述归档特征的清单文件(manifest)。
清单文件被命名为MANIFEST.MF,它位于JAR文件的一个特殊 META-INF子目录中。最小的符合标准的清单文件是很简单的:
Manifest-Version: 1.0
复杂的清单文件可能包含更多条目。这些清单条目被分成多个节。第一节被称为主节( main section)。它作用于整个JAR文件。随后的条目用来指定已命名条目的属性,这些已命名的条目可以是某个文件、包或者URL。它们都必须起始于名为Name的条目。节与节之间用空行分开。例如:
Manifest-Version: 1.0
描述这个归档文件的行
Name: woozle.class
描述这个文件的行
Name: com/mycompany/mypkg/ -
可执行JAR文件
可以使用jar命令中的e选项指定程序的入口点,即通常需要在调用java程序加载器时指定的类:
jar cvfe MyProgram.jar com.mycompany.mypkg.MainAppC1ass files to add
或者,可以在清单中指定应用程序的主类,包括以下形式的语句:
Main-Class: com.mycompany.mypkg.MainAppCTass
不要将扩展名.class添加到主类名中。 -
资源
类加载器知道如何搜索类文件,直到在类路径、存档文件或web服务器上找到为止。利用资源机制,对于非类文件也可以同样方便地进行操作。下面是必要的步骤:- 获得具有资源的Class对象,例如,AboutPanel.class。
- 如果资源是一个图像或声音文件,那么就需要调用getresource (filename)获得作为URL的资源位置,然后利用getImage或getAudioClip方法进行读取。
- 与图像或声音文件不同,其他资源可以使用getResourceAsStream方法读取文件中的数据。
重点在于类加载器可以记住如何定位类,然后在同一位置查找关联的资源。
-
密封
要想密封一个包,需要将包中的所有类放到一个JAR文件中。在默认情况下,JAR文件中的包是没有密封的。可以在清单文件的主节中加入下面一行:
Sealed: true
来改变全局的默认设定。对于每个单独的包,可以通过在JAR文件的清单中增加一节,来指定是否想要密封这个包。例如:
Name: com/mycompany/uti1/
Sealed: true
Name: com/mycompany/misc/
Sealed: false
要想密封一个包,需要创建一个包含清单指令的文本文件。然后用常规的方式运行jar命令:
jar cvfm MyArchive.jar manifest.mf files to add
2. 应用首选项的存储
-
属性映射
属性映射( property map)是一种存储键/值对的数据结构。属性映射通常用来存储配置信息,它有3个特性:- 键和值是字符串。
- 映射可以很容易地存入文件以及从文件加载。
- 有一个二级表保存默认值。
实现属性映射的Java类名为Properties。
可以使用store方法将属性映射列表保存到一个文件中。在这里,我们将属性映射保在文件 program.properties中。第二个参数是包含在这个文件中的注释。
Properties settings = new Properties(;settings.setProperty(“width”,“200"”);
settings.setProperty(“title”,“He11o,wor1d!”);
OutputStream out = new FileOutputStream(“program.properties”);
settings.store(out,“Program Properties”);
这个示例会给出以下输出:
#Program Properties
#Mon Apr 30 07:22:52 2007
width=200
title=He11o, world!
要从文件加载属性,可以使用以下调用:
InputStream in = new FileInputStream(“program.properties”);settings.load(in); -
首选项API
我们已经看到,利用Properties类可以很容易地加载和保存配置信息。不过,使用属性文件有以下缺点:- 有些操作系统没有主目录的概念,所以很难找到一个统一的配置文件位置。
- 关于配置文件的命名没有标准约定,用户安装多个Java应用时,就更容易发生命名冲突。
Preferences存储库有一个树状结构,节点路径名类似于/com/mycompany/myapp。类似于包名,只要程序员用逆置的域名作为路径的开头,就可以避免命名冲突。实际上,API的设计者就建议配置节点路径要与程序中的包名一致。
若要访问树中的一个节点,需要从用户或系统根开始:
Preferences root = Preferences.userRoot(;
或
Preferences root = Preferences.systemRootO;
然后访问节点。可以直接提供一个节点路径名:
Preferences node = root.node("/com/mycompany/myapp");
如果节点的路径名等于类的包名,还有一种便捷方式来获得这个节点。只需要得到这个作的一个对象,然后调用:
Preferences node = Preferences. userNodeForPackage(obj.getClass();
或
Preferences node = Preferences.systemNodeForPackage(obj.getClassO);
一般来说,obj往往是this引用。
一旦得到了节点,可以用以下方法访问键/值表:
String get(String key,String defval)
int getInt(String key,int defval)
long getLong(String key,long defval)float getFloat(String key,float defval)
double getDouble(String key,double defval)
boolean getBoolean(String key,boolean defva1)
byte[] getByteArray(String key, byte[] defval)
需要说明的是,读取信息时必须指定一个默认值,以防止没有可用的存储库数据。
3. 服务和加载器
4. applet
-
一个简单的applet
要执行applet,需要完成以下3个步骤:
- 将Java源文件编译为类文件。
- 将类打包到一个JAR文件中。
- 创建一个HTML文件,告诉浏览器首先加载哪个类文件,以及如何设定applet 的大小。
<applet class="applet/NotHe1loworld.class"archive-="NotHe11oworld.jar"width="300”height="300"></applet>
很容易把一个图形化Java应用转换为可以嵌入在Web页面中的 applet。基本上来说,所有用户界面代码都可以保持不变。下面给出具体的步骤:
- 建立一个HTML页面,其中包含加载applet 代码的适当标记。
- 提供 JApplet类的一个子类。将这个类标记为public。否则applet将无法加载。
- 删去应用中的main方法。不要为应用构造框架窗口。你的应用将在浏览器中显示。
- 把所有初始化代码从框架窗口移至applet 的 init方法。不需要明确构造applet对象,浏览器会实例化 applet对象并调用init方法。
- 删除setSize调用;对 applet来说,用HTML文件中的width和 height参数就可以指定大小。
- 删除setDefaultCloseOperation调用。applet 不能关闭;浏览器退出时applet 就会终止运行。
- 如果应用调用setTitle,则删除这个方法调用。applet没有标题栏。(当然,可以用HTML title标记为Web页面本身指定标题。)
- 不要调用setVisible(true)。applet会自动显示。
-
applet HTML属性
- width,height:这些属性是必要的,指定了applet的宽度和高度(单位为像素)。
- align:这个属性指定了applet的对齐方式。属性值与HTML img标记的 align属性值相同。
- 这些属性是可选的,指定了applet上下的像素数( vspace)以及左右两边的像素数(hspace)。
- code:这个属性指定了applet的类文件名。路径名必须与 applet类的包一致。例如,如果 applet类在包com.mycompany中,那么这个属性就是code=“com/mycompany/MyApplet.class”,也可以是code=“com.mycompany.MyApplet.class”。
- archive:这个属性会列出包含applet的类以及其他资源的JAR文件(可能有多个JAR文件)。这些文件会在加载applet之前从 Web服务器获取。JAR文件用逗号分隔。
- codebase:这个属性是加载JAR文件(早期还可以加载类文件)的URL。
- alt:Java禁用时,可以使用alt属性来显示一个消息。
- name:编写脚本的人可以为applet指定一个name属性,用来指示所编写的 applet。Netscape和 Internet Explorer都允许通过JavaScript调用页面上的applet的方法。
-
使用参数向applet传递信息
package Test.Applet;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.font.FontRenderContext;
import java.awt.font.LineMetrics;
import java.awt.geom.Rectangle2D;
import javax.swing.JApplet;
import javax.swing.JComponent;
public class Chart extends JApplet {
@Override
public void init() {
EventQueue.invokeLater(() -> {
String number = getParameter("values");
if (number == null)
return;
int n = Integer.parseInt(number);
double[] values = new double[n];
String[] names = new String[n];
for (int i = 0; i < n; i++) {
values[i] = Double.parseDouble("value" + i + 1);
names[i] = getParameter("name" + i + 1);
}
add(new ChartComponent(values, names, getParameter("title")));
});
}
}
class ChartComponent extends JComponent {
private double[] values;
private String[] names;
private String title;
public ChartComponent(double[] values, String[] names, String title) {
this.values = values;
this.names = names;
this.title = title;
}
public void paintComponent(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
if (values == null)
return;
double minValue = 0;
double maxValue = 0;
for (double value : values) {
if (minValue > value)
minValue = value;
if (maxValue < value)
maxValue = value;
}
if (minValue == maxValue)
return;
int panelWidth = getWidth();
int panelHeight = getHeight();
Font titleFont = new Font("Sanserif", Font.BOLD, 20);
Font labelFont = new Font("Sanserif", Font.PLAIN, 10);
FontRenderContext context = g2.getFontRenderContext();
Rectangle2D titleBounds = titleFont.getStringBounds(title, context);
double titleWidth = titleBounds.getWidth();
double top = titleBounds.getHeight();
double y = -titleBounds.getY();
double x = (panelWidth - titleWidth) / 2;
g2.setFont(titleFont);
g2.drawString(title, (float) x, (float) y);
LineMetrics labelMetrics = labelFont.getLineMetrics("", context);
double buttom = labelMetrics.getHeight();
y = panelHeight - labelMetrics.getDescent();
g2.setFont(labelFont);
double scale = (panelHeight - top - buttom) / (maxValue - minValue);
int barWidth = panelWidth / values.length;
for (int i = 0; i < names.length; i++) {
double x1 = i * barWidth + 1;
double y1 = top;
double height = values[i] * scale;
if (values[i] >= 0)
y1 += (maxValue - values[i]) * scale;
else {
y1 += maxValue * scale;
height = -height;
}
Rectangle2D rectangle2d = new Rectangle2D.Double(x1, y1, barWidth - 2, height);
g2.setPaint(Color.RED);
g2.fill(rectangle2d);
g2.setPaint(Color.BLUE);
g2.draw(rectangle2d);
Rectangle2D labaleBounds = labelFont.getStringBounds(names[i], context);
double lableWidth = labaleBounds.getWidth();
x = x1 + (barWidth - lableWidth) / 2;
g2.drawString(names[i], (float) x, (float) y);
}
}
}
<applet code="Chart.class" width="400" height="300">
<param name="title" value="Diameters of the Planets" />
<param name="values" value="9">
<param name="name1" value="Mercury">
<param name="name2" value="Venus">
<param name="name3" value="Earth">
<param name="name4" value="Mars">
<param name="name5" value="Jupter">
<param name="name6" value="Saturn">
<param name="name7" value="Urans">
<param name="name8" value="Neptune">
<param name="name9" value="Pluto">
<param name="value1" value="3100">
<param name="value2" value="7500">
<param name="value3" value="8000">
<param name="value4" value="4200">
<param name="valu5" value="88000">
<param name="value6" value="71000">
<param name="value7" value="32000">
<param name="value8" value="30600">
<param name="value9" value="1430">
</applet>
-
访问图像和音频
applet可以处理图像和音频。写作这本书时,图像必须是GIF、PNG或JPEG格式,音频文件必须是AU、AIFF、WAV或MIDI。另外也支持动画GIF,可以显示动画。
要用相对URL指定图像和音频文件的位置。通常可以通过调用getDocumentBase或getCodeBase方法得到基URL。前一个方法会得到包含这个applet的HTML页面的URL,后者会得到applet 的 codebase属性指定的URL。
可以为getImage或getAudioClip方法提供基URL和文件位置。例如:
Image cat = getImage(getDocumentBase(,“images/cat.gif”);
AudioClip meow = getAudioClip(getDocumentBase(, “audio/meow.au”); -
applet上下文
要与浏览器通信,applet可以调用getAppletContext方法。这个方法会返回一个实现AppletContext接口的对象。可以认为AppletContext接口的具体实现是applet 与外围浏览器之间的一个通信渠道。 -
applet之间通信
如果为HTML文件中的各个applet指定name属性,可以使用AppletContext接口的getApplet方法来得到这个applet 的一个引用。
-
在浏览器中显示信息项
可以访问外围浏览器的两个区域:状态栏和 Web页面显示区,这都要使用AppletContext接口的方法。
可以用showStatus方法在浏览器底部的状态栏中显示一个字符串。例如:
showStatus(“Loading data . . . please wait”);可以用showDocument方法告诉浏览器显示一个不同的Web页面。有很多方法可以达到这个目的。最简单的办法是调用showDocument并提供一个参数,即你想要显示的URL:
URL u = new URL(“http://horstmann.com/index.htm7”);
getAppletContext(.showDocument(u);
这个调用的问题在于,它会在当前页面所在的同一个窗口中打开新Web页面,因此会替换你的applet。要返回原来的 applet,用户必须点击浏览器的后退按钮。
可以在showDocument调用中提供第二个参数告诉浏览器在另一个窗口中显示文档。如果提供了特殊字符串"_blank",浏览器会用这个文档打开一个新窗口,而不是替换当前文档。更重要的是,如果利用HTML中的框架特性,可以把一个浏览器窗口分割为多个框架,每个框架都有自己的名字。可以把 applet放在一个框架中,让它在其他框架中显示文档。下一节会给出这样的一个例子。
showDocument方法
目标参数 | 位置 |
---|---|
"_self"或无 | 在当前框架中显示文档 |
"_parent" | 在父框架中显示文档 |
"_top" | 在最顶层框架中显示文档 |
"_blank" | 在新的未命名顶层窗口中显示文档 |
其他字符串 | 在指定框架中显示。如果不存在指定名字的框架, 则打开一个新的窗口,并指定为这个窗口的名字 |
-
沙箱
具体来说,沙箱中的程序有以下限制:- 它们绝对不能运行任何本地可执行程序。
- 它们不能读写本地计算机的文件系统。
- 它们不能查找有关本地计算机的信息,不过所用的Java版本和一些无害的操作系统细节除外。特别是,沙箱中的代码不能查找用户的名字、e-mail 地址,等等。
- 远程加载的程序需要用户同意与下载这个程序的服务器以外的主机通信;这个服务器称为源主机 ( originating host)。这个规则通常称为“远程代码只能与家人通话”。这个规则可以保护用户防止代码刺探内部网资源。
- 所有弹出窗口都带有一个警告消息。这个消息是一个安全特性,确保用户不会把这个窗口误认为一个本地应用。这是因为会担心不设防的用户可能访问一个Web页面,被诱骗运行远程代码,然后键入密码或信用卡号,这可能会发回给Web服务器。在早期版本的JDK中,这个消息看上去很严重:“不能信任的Java Applet窗口”。后来的版本把这个警告稍稍放松了一点:“未授权的Java Applet窗口”,后来又改为“警告:Java Applet窗口”。现在是一个警告性的小三角,只有最敏锐的用户才会注意到。
沙箱概念没有原先那么有意义。过去,任何人都可能部署沙箱代码,只有需要得到许可在沙箱外执行的代码才需要有数字签名。如今,不论是否在沙箱中运行,通过Java Plug-in执行的所有代码都必须有数字签名。
-
签名代码
applet或Java Web Start应用的JAR文件必须有数字签名。签名的JAR文件带有一个证书,指示签名者的身份。加密技术可以确保这个证书不可伪造,而且能检测出对签名文件的任何篡改。
5. Java Web Start
Java Web Start是一项在Internet上发布应用程序的技术。Java Web Start应用程序包含下列主要特性:
- Java Web Start应用程序一般通过浏览器发布。只要Java Web Start应用程序下载到本地就可以启动它,而不需要浏览器。
- Java Web Start应用程序并不在浏览器窗口内。它将显示在浏览器外的一个属于自己的框架中。
- Java Web Start应用程序不使用浏览器的Java实现。浏览器只是在加载Java Web Start应用程序描述符时启动一个外部应用程序。这与启动诸如 Adobe Acrobat或RealAudio这样的辅助应用程序所使用的机制一样。
- 数字签名应用程序可以被赋予访问本地机器的任意权限。未签名的应用程序只能运行在“沙箱”中,它可以阻止具有潜在危险的操作。
-
发布Java Web Start 应用
要想准备一个通过Java Web Start 发布的应用程序,应该将其打包到一个或多个JAR文件中。然后创建一个Java Network Launch Protocol (JNLP)格式的描述符文件。将这些文件放置在Web服务器上。
-
JNLP API
API提供了下面的服务:- 加载和保存文件
- 访问剪贴板打印
- 下载文件
- 在默认的浏览器中显示一个文档
- 保存和获取持久性配置信息
- 确信只运行一个应用程序的实例
要访问服务,需要使用ServiceManager,如下所示:
FileSaveService service = (FileSaveService) ServiceManager.lookup(“javax.jnlp.FileSaveService”);
如果服务不可用,调用将抛出 UnavailableServiceException。
十四、并发
1. 什么是线程?
单线程弹弹球案例:
package com.company.concurrency.bounce;
import com.company.ChangeDefaultUI;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.HeadlessException;
import java.awt.event.ActionListener;
/**
* Shows an animated bouncing ball(动画弹力球).
*/
public class Bounce {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
ChangeDefaultUI.changeDefaultUI();
JFrame frame = new BounceFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}
/**
* The frame with ball and buttons
*/
class BounceFrame extends JFrame {
public static final int STEPS = 1000;
public static final int DELAY = 3;
private final BallComponent ballComponent;
public BounceFrame() throws HeadlessException {
setTitle("Bounce");
ballComponent = new BallComponent();
add(ballComponent, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel();
addButton(buttonPanel, "Start", e -> addBall());
addButton(buttonPanel, "Close", event -> System.exit(0));
add(buttonPanel, BorderLayout.SOUTH);
pack();
}
private void addButton(Container container, String buttonName, ActionListener listener) {
JButton button = new JButton(buttonName);
button.addActionListener(listener);
container.add(button);
}
private void addBall() {
try {
Ball ball = new Ball();
ballComponent.add(ball);
for (int i = 0; i < STEPS; i++) {
ball.move(ballComponent.getBounds());
ballComponent.paint(ballComponent.getGraphics());
Thread.sleep(DELAY);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package com.company.concurrency.bounce;
import javax.swing.JPanel;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.util.ArrayList;
import java.util.List;
public class BallComponent extends JPanel {
private static final int DEFAULT_WIDTH = 450;
private static final int DEFAULT_HEIGHT = 350;
private List<Ball> balls = new ArrayList<>();
public void add(Ball b) {
balls.add(b);
}
public void paintComponent(Graphics graphics) {
super.paintComponent(graphics);
Graphics2D graphics2D = (Graphics2D) graphics;
for (Ball b :
balls) {
graphics2D.fill(b.getShape());
}
}
@Override
public Dimension getPreferredSize() {
return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
}
package com.company.concurrency.bounce;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
public class Ball {
private static final int X_SIZE = 15;
private static final int Y_SIZE = 15;
private double x = 0;
private double y = 0;
private double dx = 1;
private double dy = 1;
public void move(Rectangle2D bounds) {
x += dx;
y += dy;
if (x < bounds.getMinX()) {
x = bounds.getMinX();
dx = -dx;
}
if (x + X_SIZE >= bounds.getMaxX()) {
x = bounds.getMaxX() - X_SIZE;
dx = -dx;
}
if (y < bounds.getMinY()) {
y = bounds.getMinY();
dy = -dy;
}
if (y + Y_SIZE >= bounds.getMaxY()) {
y = bounds.getMaxY() - Y_SIZE;
dy = -dx;
}
}
public Ellipse2D getShape() {
return new Ellipse2D.Double(x, y, X_SIZE, Y_SIZE);
}
}
- 使用线程给其他任务提供机会
下面是在一个单独的线程中执行一个任务的简单过程:- 将任务代码移到实现了Runnable接口的类的run方法中。这个接口非常简单,只有一个方法:
public interface Runnable{
void run();
}
由于Runnable是一个函数式接口,可以用lambda表达式建立一个实例:
Runnable r = () -> { task code }; - 由Runnable创建一个 Thread对象:
Thread t = new Thread®; - 启动线程:t.start();
- 将任务代码移到实现了Runnable接口的类的run方法中。这个接口非常简单,只有一个方法:
private void addBall() {
Ball ball = new Ball();
ballComponent.add(ball);
Runnable runnable = () -> {
try {
for (int i = 0; i < STEPS; i++) {
ball.move(ballComponent.getBounds());
ballComponent.paint(ballComponent.getGraphics());
Thread.sleep(DELAY);
}
} catch (InterruptedException e) {
System.out.println(e.getMessage());
}
};
Thread thread = new Thread(runnable);
thread.start();
}
2. 中断线程
当线程的run方法执行方法体中最后一条语句后,并经由执行return语句返回时,或者出现了在方法中没有捕获的异常时,线程将终止。
没有可以强制线程终止的方法。然而,interrupt方法可以用来请求终止线程。
当对一个线程调用interrupt方法时,线程的中断状态将被置位。这是每一个线程都具有的 boolean标志。每个线程都应该不时地检查这个标志,以判断线程是否被中断。
要想弄清中断状态是否被置位,首先调用静态的Thread.currentThread方法获得当前线程,然后调用isInterrupted方法:
while (!Thread.currentThread().isInterrupted() && more work to do){
do more work
}
但是,如果线程被阻塞,就无法检测中断状态。这是产生InterruptedException异常的地方。当在一个被阻塞的线程(调用sleep或wait)上调用interrupt方法时,阻塞调用将会被Interrupted Exception异常中断。
没有任何语言方面的需求要求一个被中断的线程应该终止。中断一个线程不过是引起它的注意。被中断的线程可以决定如何响应中断。某些线程是如此重要以至于应该处理完异常后,继续执行,而不理会中断。但是,更普遍的情况是,线程将简单地将中断作为一个终止的请求。这种线程的run方法具有如下形式:
Runnable r = () -> {
try {
while (!Thread.currentThread().isInterrupted() && more work to do){
do more work
}catch(InterruptedException e){
// thread was interrupted during sleep or waitfinally
}finally {
//cleanup, if required
}
};
如果在每次工作迭代之后都调用sleep方法(或者其他的可中断方法),isInterrupted检测既没有必要也没有用处。如果在中断状态被置位时调用sleep方法,它不会休眠。相反,它将清除这一状态(!)并抛出InterruptedException。因此,如果你的循环调用sleep,不会检测中断状态。相反,要如下所示捕获 InterruptedException异常:
Runnable r = () -> {
try {
while(more work to do) {
…
Thread.sleep(delay);
}
}catch(InterruptedException e) {
…
}finally{
…
}
};
注释:有两个非常类似的方法,interrupted和 isInterrupted。Interrupted方法是一个静态方法,它检测当前的线程是否被中断。而且,调用interrupted方法会清除该线程的中断状态。另一方面,isInterrupted方法是一个实例方法,可用来检验是否有线程被中断。调用这个方法不会改变中断状态。
在很多发布的代码中会发现InterruptedException异常被抑制在很低的层次上,像这样:
void mySubTask(){
…
try { sleep(delay);}
catch (InterruptedException e) // Don’t ignore!
}
不要这样做!如果不认为在catch子句中做这一处理有什么好处的话,仍然有两种合理约选择:
- 在catch子句中调用Thread.currentThread().interrupt()来设置中断状态。于是,调用者可以对其进行检测。
void mySubTask(){
...
try { sleep(delay);}
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
...
}
- 或者,更好的选择是,用throws InterruptedException标记你的方法,不采用try语句块捕获异常。于是,调用者(或者,最终的run)可以捕获这一异常。
void mySubTask() throws InterruptedException{
...
}
3. 线程状态
线程可以有如下6种状态:
- New(新创建)
- Runnable (可运行)
- Blocked(被阻塞)
- Waiting(等待)
- Timed waiting(计时等待)
- Terminated(被终止)
要确定一个线程的当前状态,可调用getState方法。
-
新创建线程
当用new操作符创建一个新线程时,如new Thread®,该线程还没有开始运行。这意味着它的状态是new。当一个线程处于新创建状态时,程序还没有开始运行线程中的代码。在线程运行之前还有一些基础工作要做。 -
可运行线程
一旦调用start方法,线程处于runnable状态。一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。(Java 的规范说明没有将它作为一个单独状态。一个正在运行中的线程仍然处于可运行状态。)记住,在任何给定时刻,一个可运行的线程可能正在运行也可能没有运行(这就是为什么将这个状态称为
可运行
而不是运行
)。 -
被阻塞线程和等待线程
当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资源。直到线程调度器重新激活它。细节取决于它是怎样达到非活动状态的。- 当一个线程试图获取一个内部的对象锁(而不是java.util.concurrent库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。
- 当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调用Object.wait方法或Thread.join方法,或者是等待java.util.concurrent库中的Lock或Condition时,就会出现这种情况。实际上,被阻塞状态与等待状态是有很大不同的。
- 有几个方法有一个超时参数。调用它们导致线程进入
计时等待( timed waiting)状态
。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有Thread.sleep和Object.wait、Thread.join、Lock.tryLock 以及 Condition.await 的计时版。
当一个线程被阻塞或等待时(或终止时),另一个线程被调度为运行状态。当一个线程被重新激活(例如,因为超时期满或成功地获得了一个锁),调度器检查它是否具有比当前运行线程更高的优先级。如果是这样,调度器从当前运行线程中挑选一个,剥夺其运行权,选择一个新的线程运行。
-
被终止的线程
线程因如下两个原因之一而被异常终止:- 因为run方法正常退出而自然死亡。
- 因为一个没有捕获的异常终止了run方法而意外死亡。
特别是,可以调用线程的stop方法杀死一个线程。该方法抛出ThreadDeath错误对象,由此杀死线程。但是,stop方法已过时,不要在自己的代码中调用这个方法。
4. 线程属性
-
线程优先级
在Java程序设计语言中,每一个线程有一个优先级。默认情况下,一个线程继承它的父线程的优先级。可以用setPriority方法提高或降低任何一个线程的优先级。可以将优先级设置为在MIN_PRIORITY(在 Thread类中定义为1)与MAX_PRIORITY(定义为10)之间的任何值。NORM_PRIORITY 被定义为5。
每当线程调度器有机会选择新线程时,它首先选择具有较高优先级的线程。但是,线程优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时,Java线程的优先级被映射到宿主机平台的优先级上,优先级个数也许更多,也许更少。
初级程序员常常过度使用线程优先级。为优先级而烦恼是事出有因的。不要将程序构建为功能的正确性依赖于优先级。警告:如果确实要使用优先级,应该避免初学者常犯的一个错误。如果有几个高优先级的线程没有进入非活动状态,低优先级的线程可能永远也不能执行。每当调度器决定运行一个新线程时,首先会在具有高优先级的线程中进行选择,尽管这样会使低优先级的线程完全饿死。
-
守护线程
可以通过调用
t.setDaemon(true);
将线程转换为守护线程( daemon thread)。这样一个线程没有什么神奇。守护线程的唯一用途是为其他线程提供服务。计时线程就是一个例子,它定时地发送“计时器嘀嗒”信号给其他线程或清空过时的高速缓存项的线程。当只剩下守护线程时,虚拟机就退出了,由于如果只剩下守护线程,就没必要继续运行程序了。
守护线程有时会被初学者错误地使用,他们不打算考虑关机( shutdown)动作。但是,这是很危险的。守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。 -
未捕获异常处理器
线程的run方法不能抛出任何受查异常,但是,非受查异常会导致线程终止。在这种情况下,线程就死亡了。
但是,不需要任何catch子句来处理可以被传播的异常。相反,就在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。
该处理器必须属于一个实现Thread.UncaughtExceptionHandler接口的类。这个接口只有一个方法。
void uncaughtException(Thread t,Throwable e)
可以用setUncaughtExceptionHandler方法为任何线程安装一个处理器。也可以用Thread类的静态方法setDefaultUncaughtExceptionHandler为所有线程安装一个默认的处理器。替换处理器可以使用日志API发送未捕获异常的报告到日志文件。
如果不安装默认的处理器,默认的处理器为空。但是,如果不为独立的线程安装处理器,此时的处理器就是该线程的 ThreadGroup对象。ThreadGroup类实现Thread.UncaughtExceptionHandler接口。它的uncaughtException方法做如下操作:
- 如果该线程组有父线程组,那么父线程组的uncaughtException方法被调用。
- 否则,如果 Thread.getDefaultExceptionHandler方法返回一个非空的处理器,则调用该处理器。
- 否则,如果 Throwable是 ThreadDeath的一个实例,什么都不做。
- 否则,线程的名字以及Throwable 的栈轨迹被输出到System.err 上。
5. 同步
竞态条件(race condition)
在大多数实际的多线程应用中,两个或两个以上的线程需要共享对同一数据的存取。
- 静态条件案例
在下面的测试程序中,模拟一个有若干账户的银行。随机地生成在这些账户之间转移钱款的交易。每一个账户有一个线程。每一笔交易中,会从线程所服务的账户中随机转移一定数目的钱款到另一个随机账户。
package com.company.concurrency.unsynch;
public class UnsynchBankTest {
public static final int ACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static final double MAX_AMOUNT = 1000;
public static final int DELAY = 10;
public static void main(String[] args) {
Bank bank = new Bank(ACCOUNTS, INITIAL_BALANCE);
for (int i = 0; i < ACCOUNTS; i++) {
int fromAccount = i;
Runnable runnable = () -> {
try {
int toAccount = (int) (bank.size() * Math.random());
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}
}
package com.company.concurrency.unsynch;
import java.util.Arrays;
public class Bank {
private final double[] accounts;
public Bank(int accounts, double initialBalance) {
this.accounts = new double[accounts];
Arrays.fill(this.accounts, initialBalance);
}
public double size() {
return accounts.length;
}
public void transfer(int fromAccount, int toAccount, double amount) {
if (accounts[fromAccount] < amount)
return;
System.out.print(Thread.currentThread());
accounts[fromAccount] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, fromAccount, toAccount);
accounts[toAccount] += amount;
System.out.printf(" Total Balance:%10.2f\n", getTotalBalance());
}
private double getTotalBalance() {
double sum = 0;
for (double account :
accounts) {
sum += account;
}
return sum;
}
}
-
竞态条件详解
-
锁对象
有两种机制防止代码块受并发访问的干扰。Java语言提供一个synchronized关键字达到这一目的,并且Java SE 5.0引入了ReentrantLock类。synchronized关键字自动提供一个锁以及相关的“条件”,对于大多数需要显式锁的情况,这是很便利的。
用ReentrantLock保护代码的基本结构如下:
myLock.lock();
try{
critical section
}finally{
myLock.unlock();
}
这一结构确保任何时刻只有一个线程进入临界区。一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象。锁是可重入的,因为线程可以重复地获得已经持有的锁。锁保持一个持有计数( holdcount)来跟踪对lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
-
条件对象
通常,线程进入临界区,却发现在某一条件满足之后它才能执行。要使用一个条件对象来管理那些已经获得了一个锁但是却不能做有用工作的线程。在这一节里,我们介绍Java库中条件对象的实现。(由于历史的原因,条件对象经常被称为条件变量( conditionalvariable)
。)
一个锁对象可以有一个或多个相关的条件对象。你可以用newCondition方法获得一个条件对象。习惯上给每一个条件对象命名为可以反映它所表达的条件的名字。
例如,在此设置一个条件对象来表达“余额充足”条件:
class Bank{
private Condition sufficientFunds;
private Lock bankLock = new ReentrantLock();
…
public Bank(){
sufficientFunds = bankLock.newCondition();
}
}
如果transfer方法发现余额不足,将调用
sufficientFunds.await();
当前线程现在被阻塞了,并放弃了锁。我们希望这样可以使得另一个线程可以进行增加账户余额的操作。
等待获得锁的线程和调用await方法的线程存在本质上的不同。一旦一个线程调用await方法,它进入该条件的等待集。当锁可用时,该线程不能马上解除阻塞。相反,它处于阻塞状态,直到另一个线程调用同一条件上的signalAl1方法时为止。
当另一个线程转账时,它应该调用
sufficientFunds.signalAll();
这一调用重新激活因为这一条件而等待的所有线程。当这些线程从等待集当中移出时,它们再次成为可运行的,调度器将再次激活它们。同时,它们将试图重新进入该对象。一旦锁成为可用的,它们中的某个将从await调用返回,获得该锁并从被阻塞的地方继续执行。
此时,线程应该再次测试该条件。由于无法确保该条件被满足———signalAll方法仅仅是通知正在等待的线程:此时有可能已经满足条件,值得再次去检测该条件。
至关重要的是最终需要某个其他线程调用signalAll方法。当一个线程调用await时,它没有办法重新激活自身。它寄希望于其他线程。如果没有其他线程来重新激活等待的线程,它就永远不再运行了。这将导致令人不快的死锁( deadlock)现象。如果所有其他线程被阻塞,最后一个活动线程在解除其他线程的阻塞状态之前就调用await方法,那么它也被阻塞。没有任何线程可以解除其他线程的阻塞,那么该程序就挂起了。
应该何时调用signalAll 呢?经验上讲,在对象的状态有利于等待线程的方向改变时调用signalAll。例如,当一个账户余额发生改变时,等待的线程会应该有机会检查余额。在例子中,应该在完成转账时调用signalAll方法。
另一个方法 signal,则是随机解除等待集中某个线程的阻塞状态。这比解除所有线程的阻塞更加有效,但也存在危险。如果随机选择的线程发现自己仍然不能运行,那么它再次被阻塞。如果没有其他线程再次调用signal,那么系统就死锁了。
package com.company.concurrency.unsynch;
import java.util.Arrays;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Bank {
private final double[] accounts;
private final Lock reentrantLock = new ReentrantLock();
private final Condition sufficientFunds;
public Bank(int accounts, double initialBalance) {
this.accounts = new double[accounts];
Arrays.fill(this.accounts, initialBalance);
sufficientFunds = reentrantLock.newCondition();
}
public double size() {
return accounts.length;
}
public void transfer(int fromAccount, int toAccount, double amount) {
reentrantLock.lock();
try {
while (accounts[fromAccount] < amount) {
sufficientFunds.await();
}
System.out.print(Thread.currentThread());
accounts[fromAccount] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, fromAccount, toAccount);
accounts[toAccount] += amount;
System.out.printf(" Total Balance:%10.2f\n", getTotalBalance());
sufficientFunds.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
private double getTotalBalance() {
reentrantLock.lock();
try {
double sum = 0;
for (double account : accounts) {
sum += account;
}
return sum;
}finally {
reentrantLock.unlock();
}
}
}
package com.company.concurrency.unsynch;
import java.util.concurrent.locks.ReentrantLock;
public class UnsynchBankTest {
public static final int ACCOUNTS = 100;
public static final double INITIAL_BALANCE = 1000;
public static final double MAX_AMOUNT = 1000;
public static final int DELAY = 10;
public static void main(String[] args) {
Bank bank = new Bank(ACCOUNTS, INITIAL_BALANCE);
for (int i = 0; i < ACCOUNTS; i++) {
int fromAccount = i;
Runnable runnable = () -> {
try {
int toAccount = (int) (bank.size() * Math.random());
double amount = MAX_AMOUNT * Math.random();
bank.transfer(fromAccount, toAccount, amount);
Thread.sleep((int) (DELAY * Math.random()));
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Thread thread = new Thread(runnable);
thread.start();
}
}
}
- synchronized关键字
总接一下有关锁和条件的关键之处:- 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
- 锁可以管理试图进入被保护代码段的线程。
- 锁可以拥有一个或多个相关的条件对象。
- 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程。
Lock 和 Condition接口为程序设计人员提供了高度的锁定控制。
然而,大多数情况下,并不需要那样的控制,并且可以使用一种嵌入到Java语言内部的机制。从1.0版开始,Java中的每一个对象都有一个内部锁。如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的对象锁。
换句话说
public synchronized void method(){
…
}
等价于
public void method(){
this.intrinsiclock.lock();
try{
…
}finally{this.intrinsiclock.unlock();}
}
内部对象锁只有一个相关条件。wait方法添加一个线程到等待集中,notifyAll /notify方法解除等待线程的阻塞状态。换句话说,调用wait或notifyAll等价于
intrinsicCondition.await();
intrinsicCondition.signalAll();
public synchronized void transfer(int from, int to, double amount) {
try {
while (accounts[from] < amount) {
wait();
}
System.out.print(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" %10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance:%10.2f\n", getTotalBalance());
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
将静态方法声明为synchronized也是合法的。如果调用这种方法,该方法获得相关的类对象的内部锁。例如,如果Bank类有一个静态同步的方法,那么当该方法被调用时,Bank.class对象的锁被锁住。因此,没有其他线程可以调用同一-个类的这个或任何其他的同步静态方法。
内部锁和条件存在一些局限。包括:
- 不能中断一个正在试图获得锁的线程。
- 试图获得锁时不能设定超时。
- 每个锁仅有单一的条件,可能是不够的。
在代码中应该使用哪一种?Lock和 Condition对象还是同步方法﹖下面是一些建议: - 最好既不使用Lock/Condition也不使用synchronized关键字。在许多情况下你可以使用java.util.concurrent包中的一种机制,它会为你处理所有的加锁.
- 如果synchronized关键字适合你的程序,那么请尽量使用它,这样可以减少编写的代码数量,减少出错的几率。
- 如果特别需要Lock/Condition结构提供的独有特性时,才使用Lock/Condition.
-
同步阻塞
正如刚刚讨论的,每一个Java对象有一个锁。线程可以通过调用同步方法获得锁。还有另一种机制可以获得锁,通过进入一个同步阻塞。当线程进入如下形式的阻塞:
synchronized (obj){// this is the syntax for a synchronized block
critical section
}
于是它获得obj的锁。(不推荐使用) -
监视器概念
锁和条件是线程同步的强大工具,但是,严格地讲,它们不是面向对象的。多年来,研究人员努力寻找一种方法,可以在不需要程序员考虑如何加锁的情况下,就可以保证多线程的安全性。最成功的解决方案之一是监视器(monitor),这一概念最早是由Per Brinch Hansen和Tony Hoare在20世纪70年代提出的。用Java的术语来讲,监视器具有如下特性:- 监视器是只包含私有域的类。
- 每个监视器类的对象有一个相关的锁。
- 使用该锁对所有的方法进行加锁。换句话说,如果客户端调用obj.method(),那么obj对象的锁是在方法调用开始时自动获得,并且当方法返回时自动释放该锁。因为所有的域是私有的,这样的安排可以确保一个线程在对对象操作时,没有其他线程能访问该域。
- 该锁可以有任意多个相关条件。
-
Volatile域
有时,仅仅为了读写一个或两个实例域就使用同步,显得开销过大了。毕竟,什么地方能出错呢?遗憾的是,使用现代的处理器与编译器,出错的可能性很大。- 多处理器的计算机能够暂时在寄存器或本地内存缓冲区中保存内存中的值。结果是,运行在不同处理器上的线程可能在同一个内存位置取到不同的值。
- 编译器可以改变指令执行的顺序以使吞吐量最大化。这种顺序上的变化不会改变代码语义,但是编译器假定内存的值仅仅在代码中有显式的修改指令时才会改变。然而,内存的值可以被另一个线程改变!
volatile关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。
-
final变量
上一节已经了解到,除非使用锁或volatile修饰符,否则无法从多个线程安全地读取一个域。
还有一种情况可以安全地访问一个共享域,即这个域声明为final时。考虑以下声明:
final Map<String,Double> accounts = new HashMap<>0;
其他线程会在构造函数完成构造之后才看到这个accounts变量。
如果不使用final,就不能保证其他线程看到的是accounts更新后的值,它们可能都只是看到null,而不是新构造的HashMap。
当然,对这个映射表的操作并不是线程安全的。如果多个线程在读写这个映射表,仍然需要进行同步。 -
原子性
假设对共享变量除了赋值之外并不完成其他操作,那么可以将这些共享变量声明为volatile。
java.util.concurrent.atomic包中有很多类使用了很高效的机器级指令(而不是使用锁)来保证其他操作的原子性。例如,AtomicInteger类提供了方法 incrementAndGet和decrementAndGet,它们分别以原子方式将一个整数自增或自减。 -
死锁
有可能会因为每一个线程要等待更多的钱款存入而导致所有线程都被阻塞。这样的状态称为死锁(deadlock)。 -
线程局部变量
前面几节中,我们讨论了在线程间共享变量的风险。有时可能要避免共享变量,使用ThreadLocal辅助类为各个线程提供各自的实例。 -
锁测试与超时
线程在调用lock方法来获得另一个线程所持有的锁的时候,很可能发生阻塞。应该与谨慎地申请锁。tryLock方法试图申请一个锁,在成功获得锁后返回true,否则,立即达false,而且线程可以立即离开去做其他事情。
if(myLock.tryLock()){
…
try{
…
}finally{myLock.unlock();}
}else{
…
}
可以调用tryLock 时,使用超时参数,像这样:
if (myLock.tryLock(100,TimeUnit.MILLISECONDS)) . . .
TimeUnit是一个枚举类型,可以取的值包括SECONDS、MILLISECONDS、MICROSECONDS和 NANOSECONDS。
lock方法不能被中断。如果一个线程在等待获得一个锁时被中断,中断线程在获得锁之前一直处于阻塞状态。如果出现死锁,那么,lock方法就无法终止。
然而,如果调用带有用超时参数的tryLock,那么如果线程在等待期间被中断,将抛出InterruptedException异常。这是一个非常有用的特性,因为允许程序打破死锁。
也可以调用lockInterruptibly方法。它就相当于一个超时设为无限的 tryLock方法。在等待一个条件时,也可以提供一个超时:
myCondition.await(100,TimeUnit.MILLISECONDS))
如果一个线程被另一个线程通过调用signalAll或signal激活,或者超时时限已达到,或者线程被中断,那么await方法将返回。
如果等待的线程被中断,await方法将抛出一个InterruptedException异常。在你希望出现这种情况时线程继续等待(可能不太合理),可以使用awaitUninterruptibly方法代替await。 -
读/写锁
java.util.concurrent.locks包定义了两个锁类,我们已经讨论的ReentrantLock类和ReentrantReadWriteLock类。如果很多线程从一个数据结构读取数据而很少线程修改其中数据的话,后者是十分有用的。在这种情况下,允许对读者线程共享访问是合适的。当然,写者线程依然必须是互斥访问的。
下面是使用读/写锁的必要步骤:- 构造一个ReentrantReadWriteLock对象:
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); - 抽取读锁和写锁:
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock(); - 对所有的获取方法加读锁
- 对所有的修改方法加写锁
- 构造一个ReentrantReadWriteLock对象:
-
为什么弃用stop和suspend方法
首先来看看stop方法,该方法终止所有未结束的方法,包括run方法。当线程被终止,立即释放被它锁住的所有对象的锁。这会导致对象处于不一致的状态。例如,假定TransferThread在从一个账户向另一个账户转账的过程中被终止,钱款已经转出,却没有转入目标账户,现在银行对象就被破坏了。因为锁已经被释放,这种破坏会被其他尚未停止的线程观察到。
当线程要终止另一个线程时,无法知道什么时候调用stop方法是安全的,什么时候导致对象被破坏。因此,该方法被弃用了。在希望停止线程的时候应该中断线程,被中断的线程会在安全的时候停止。接下来,看看suspend方法有什么问题。与stop不同,suspend不会破坏对象。但是,如果用suspend挂起一个持有一个锁的线程,那么,该锁在恢复之前是不可用的。如果调用suspend方法的线程试图获得同一个锁,那么程序死锁:被挂起的线程等着被恢复,而将其挂起的线程等待获得锁。
6. 阻塞队列BlockingQueue
当试图向队列添加元素而队列已满,或是想从队列移出元素而队列为空的时候,阻塞队列( blocking queue)导致线程阻塞。在协调多个线程之间的合作时,阻塞队列是一个有用的工具。工作者线程可以周期性地将中间结果存储在阻塞队列中。其他的工作者线程移出中间结果并进一步加以修改。队列会自动地平衡负载。如果第一个线程集运行得比第二个慢,第二个线程集在等待结果时会阻塞。如果第一个线程集运行得快,它将等待第二个队列集赶上来。
阻塞队列方法
方法 | 正常动作 | 特殊情况下的动作 |
---|---|---|
add | 添加一个元素 | 如果队列满,则抛出IllegalStateException异常 |
element | 返回队列的头元素 | 如果队列为空,则抛出NoSuchElementException异常 |
offer | 添加一个元素并返回true | 如果队列满,返回false |
peek | 返回队列的头元素 | 如果队列空,返回null |
poll | 移除并返回队列的头元素 | 如果队列空,返回null |
put | 添加一个元素 | 如果队列满则阻塞 |
remove | 移除并返回头元素 | 如果队列空,则抛出NoSuchElementException异常 |
take | 移除并返回头元素 | 如果队列空,则阻塞 |
package com.company.concurrency.blockingQueue;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Scanner;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueTest {
private static final int FILE_QUEUE_SIZE = 10;
private static final int SEARCH_THREADS = 100;
private static final File DUMMY = new File("");
private static final BlockingQueue<File> queue = new ArrayBlockingQueue<>(FILE_QUEUE_SIZE);
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
System.out.println("Enter Base Directory(e.g /opt/jdk1.8.0/src): ");
String directory = scanner.nextLine();
System.out.println("Enter Key Word(e.g volatile):");
String keyWord = scanner.nextLine();
Runnable enumerator = () -> {
try {
enumerate(new File(directory));
queue.put(DUMMY);
} catch (InterruptedException exception) {
exception.printStackTrace();
}
};
new Thread(enumerator).start();
for (int i = 0; i < SEARCH_THREADS; i++) {
Runnable searcher = () -> {
try {
boolean done = false;
while (!done) {
File file = queue.take();
if (file == DUMMY) {
queue.put(file);
done = true;
} else {
search(file, keyWord);
}
}
} catch (InterruptedException | FileNotFoundException e) {
e.printStackTrace();
}
};
new Thread(searcher).start();
}
}
}
/**
* Recursively enumerate all files in a given directory and its subdirectory.
*
* @param directory the directory in which to start
*/
private static void enumerate(File directory) throws InterruptedException {
File[] files = directory.listFiles();
assert files != null;
for (File file : files) {
if (file.isDirectory()) enumerate(file);
else queue.put(file);
}
}
/**
* Searches a file for a given keyword and prints all matching lines
*
* @param file the file to search
* @param keyWord the keyword to search for
* @throws FileNotFoundException throws FileNotFoundException
*/
private static void search(File file, String keyWord) throws FileNotFoundException {
try (Scanner scanner = new Scanner(file, "UTF-8")) {
int lineNumber = 0;
while (scanner.hasNext()) {
lineNumber++;
String line = scanner.nextLine();
if (line.contains(keyWord)) {
System.out.printf("%s:%d:%s%n", file.getPath(), lineNumber, line);
}
}
}
}
}
7. 线程安全的集合
-
高效的映射、集、队列
java.util.concurrent包提供了映射、有序集和队列的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和 ConcurrentLinkedQueue。
这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争极小化。与大多数集合不同,size方法不必在常量时间内操作。确定这样的集合当前的大小通常需要遍历。有些应用使用庞大的并发散列映射,这些映射太过庞大,以至于无法用size方法得到它的大小,因为这个方法只能返回int。对于一个包含超过20亿条目的映射该如何处理?Java SE8引入了一个mappingCount方法可以把大小作为long返回。
集合返回弱一致性( weakly consistent)的迭代器。这意味着迭代器不一定能反映出它们被构造之后的所有的修改,但是,它们不会将同一个值返回两次,也不会抛出 ConcurrentModificationException异常。
之形成对照的是,集合如果在迭代器构造之后发生改变,java.util包中的迭代器
将抛出一个ConcurrentModificationException异常。 -
映射条目的原子更新
-
对并发散列映射的批操作
Java SE8为并发散列映射提供了批操作,即使有其他线程在处理映射,这些操作也能安全地执行。批操作会遍历映射,处理遍历过程中找到的元素。无须冻结当前映射的快照。除非你恰好知道批操作运行时映射不会被修改,否则就要把结果看作是映射状态的一个近似。
有3种不同的操作:- 搜索(search)为每个键或值提供一个函数,直到函数生成一个非null 的结果。然后搜索终止,返回这个函数的结果。
- 归约(reduce)组合所有键或值,这里要使用所提供的一个累加函数。
- forEach为所有键或值提供一个函数。
每个操作都有4个版本: - operationKeys:处理键。
- operationValues:处理值。
- operation:处理键和值。
- operationEntries:处理Map.Entry对象。
对于上述各个操作,需要指定一个参数化阈值( parallelism threshold)。如果映射包含的元素多于这个阈值,就会并行完成批操作。如果希望批操作在一个线程中运行,可以使用阈值Long.MAX_VALUE。如果希望用尽可能多的线程运行批操作,可以使用阈值1。
-
并发集视图
假设你想要的是一个大的线程安全的集而不是映射。并没有一个 ConcurrentHashSet类,而且你肯定不想自己创建这样一个类。当然,可以使用ConcurrentHashMap(包含“假”值),不过这会得到一个映射而不是集,而且不能应用Set接口的操作。
静态newKeySet方法会生成一个 Set,这实际上是ConcurrentHashMap<K, Boolean>的一个包装器。(所有映射值都为Boolean.TRUE,不过因为只是要把它用作一个集,所以并不关心具体的值。)
Set words = ConcurrentHashMap.newKeySet(;
当然,如果原来有一个映射,keySet方法可以生成这个映射的键集。这个集是可变的。如果删除这个集的元素,这个键(以及相应的值)会从映射中删除。不过,不能向键集增加元素,因为没有相应的值可以增加。Java SE 8为ConcurrentHashMap增加了第二个keySet方法,包含一个默认值,可以在为集增加元素时使用:
Set words = map.keySet(1L);
words.add(“Java”);
如果"Java"在 words中不存在,现在它会有一个值1。 -
写数组的拷贝
CopyOnWriteArrayList和 CopyOnWriteArraySet是线程安全的集合,其中所有的修改线程对底层数组进行复制。如果在集合上进行迭代的线程数超过修改线程数,这样的安排是很有用的。当构建一个迭代器的时候,它包含一个对当前数组的引用。如果数组后来被修改了,迭代器仍然引用旧数组,但是,集合的数组已经被替换了。因而,旧的迭代器拥有一致的(可能过时的)视图,访问它无须任何同步开销。 -
并行数组算法
在Java SE8中,Arrays类提供了大量并行化操作。静态Arrays.parallelSort方法可以对一个基本类型值或对象的数组排序。
parallelSetAll方法会用由一个函数计算得到的值填充一个数组。这个函数接收元素索引,然后计算相应位置上的值。
以十o
最后还有一个parallelPrefix方法,它会用对应一个给定结合操作的前缀的累加结果替换各个数组元素。这是什么意思?这里给出一个例子。考虑数组[1,2,3,4,…]和×操作。执行Arrays.parallelPrefix(values,(x, y) -> x*y)之后,数组将包含:
[1,1×2,1×2×3,1×2×3×4,… .] -
较早的线程安全集合
从Java的初始版本开始,Vector 和Hashtable类就提供了线程安全的动态数组和散列气.西实现。现在这些类被弃用了,取而代之的是ArrayList和HashMap类。这些类不是线程安全的,而集合库中提供了不同的机制。任何集合类都可以通过使用同步包装器( synchronizationwrapper)变成线程安全的:
List synchArrayList = Collections.synchronizedList(new ArrayList());
Map<K, V> synchHashMap = Collections.synchronizedMap(new HashMap<K,V>()); 结果集合的方法使用锁加以保护,提供了线程安全访问。
应该确保没有任何线程通过原始的非同步方法访问数据结构。最便利的方法是确保不保存任何指向原始对象的引用,简单地构造一个集合并立即传递给包装器,像我们的例子中所做的那样。
如果在另一个线程可能进行修改时要对集合进行迭代,仍然需要使用“客户端”锁定:
synchronized (synchHashMap)
{
Iterator iter = synchHashMap. keySet().iterator();
while (iter.hasNext0) . . .;
}
如果使用“for each”循环必须使用同样的代码,因为循环使用了迭代器。注意:如果在迭代过程中,别的线程修改集合,迭代器会失效,抛出 ConcurrentModificationException异常。同步仍然是需要的,因此并发的修改可以被可靠地检测出来。
最好使用java.util.concurrent包中定义的集合,不使用同步包装器中的。特别是,假如它们访问的是不同的桶,由于ConcurrentHashMap已经精心地实现了,多线程可以访问它而且不会彼此阻塞。有一个例外是经常被修改的数组列表。在那种情况下,同步的ArrayList可以胜过CopyOnWriteArrayList。
8. Callable与Future
Runnable封装一个异步运行的任务,可以把它想象成为一个没有参数和返回值的异步方法。Callable与Runnable类似,但是有返回值。Callable接口是一个参数化的类型,只有一个方法 call。
public interface CallablecV>{
V call() throws Exception;
}
类型参数是返回值的类型。例如,Callable<Integer>表示一个最终返回Integer对象的异步计算。
Future保存异步计算的结果。可以启动一个计算,将Future对象交给某个线程,然后忘掉它。Future对象的所有者在结果计算好之后就可以获得它。
Future接口具有下面的方法:
public interface FuturecV>{
V get() throws . . .;
V get(long timeout,TimeUnit unit) throws . . .;
void cancel(boolean mayInterrupt);
boolean isCancelled();
boolean isDon();
}
第一个get方法的调用被阻塞,直到计算完成。如果在计算完成之前,第二个方法的调用超时,抛出一个TimeoutException异常。如果运行该计算的线程被中断,两个方法都将抛出InterruptedException。如果计算已经完成,那么get方法立即返回。
如果计算还在进行,isDone方法返回false;如果完成了,则返回true。
可以用cancel方法取消该计算。如果计算还没有开始,它被取消且不再开始。如果计算处于运行之中,那么如果mayInterrupt参数为true,它就被中断。
FutureTask包装器是一种非常便利的机制,可将Callable转换成Future和Runnable,它同时实现二者的接口。例如:
Callable<Integer> myComputeion = .....;
FutureTask<Integer> task = new FutureTask<Integer>(myConpution);
Thread thread = new Thread(task);
thread.run;
...
Integer result = thread.get();
package com.company.concurrency.future;
import java.io.File;
import java.util.Scanner;
import java.util.concurrent.FutureTask;
public class FutureTaskTest {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
System.out.println("Enter Base Directory(e.g /opt/jdk1.8.0/src): ");
String directory = scanner.nextLine();
System.out.println("Enter Key Word(e.g volatile):");
String keyWord = scanner.nextLine();
// 计算匹配的文件数目
MatchCounter counter = new MatchCounter(new File(directory), keyWord);
FutureTask<Integer> task = new FutureTask<>(counter);
Thread thread = new Thread(task);
thread.start();
if (task.isDone()) {
System.out.println(task.get() + " matched files!");
} else {
System.out.println(task.toString() +" is counting...");
System.out.println(Thread.currentThread().getState());
System.out.println(task.get() + "matched files!");
}
} catch (Exception exception) {
exception.printStackTrace();
}
}
}
package com.company.concurrency.future;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
/**
* This task counts the files in a directory and its subdirectories that contains a given keyword.
*/
public class MatchCounter implements Callable<Integer> {
private final File directory;
private final String keyWord;
public MatchCounter(File directory, String keyWord) {
this.directory = directory;
this.keyWord = keyWord;
}
@Override
public Integer call() {
int count = 0;
try {
File[] files = directory.listFiles();
List<Future<Integer>> results = new ArrayList<>();
assert files != null;
for (File file : files) {
if (file.isDirectory()) {
MatchCounter counter = new MatchCounter(file, keyWord);
FutureTask<Integer> task = new FutureTask<>(counter);
results.add(task);
Thread thread = new Thread(task);
thread.start();
} else {
if (search(file)) count++;
}
for (Future<Integer> result : results) {
try {
count += result.get();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} catch (ExecutionException e) {
e.printStackTrace();
}
return count;
}
private boolean search(File file) {
try {
try (Scanner scanner = new Scanner(file, "UTF-8")) {
boolean found = false;
while (!found && scanner.hasNext()) {
String line = scanner.nextLine();
if (line.contains(keyWord)) found = true;
}
return found;
}
} catch (IOException e) {
return false;
}
}
}
9. 执行器
构建一个新的线程是有一定代价的,因为涉及与操作系统的交互。如果程序中创建了大量的生命期很短的线程,应该使用线程池( thread pool)。一个线程池中包含许多准备运行的空闲线程。将Runnable对象交给线程池,就会有一个线程调用run方法。当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。
另一个使用线程池的理由是减少并发线程的数目。创建大量线程会大大降低性能甚至使虚拟机崩溃。如果有一个会创建许多线程的算法,应该使用一个线程数“固定的”线程池以限制并发线程的总数。
执行器(Executors)类有许多静态工厂方法用来构建线程池,下表中对这些方法进行了汇总。
执行者工厂方法
方法 | 描述 |
---|---|
newCachedThreadPool | 必要时创建新线程;空闲线程会被保留60秒 |
newFixedThreadPool | 该池包含固定数量的线程;空闲线程会一直保留 |
newSingleThreadExecutor | 只有一个线程的“池”,该线程顺序执行每一个提交的任务 |
newScheduledThreadPool | 用于预定执行二构建的固定线程池,替代java.util.Timer |
newSingleThreadScheduledExecutor | 用于预定执行而构建的单线程“池” |
-
线程池
newCached-ThreadPool方法构建了一个线程池,对于每个任务,如果有空闲线程可用,立即让它执行任务,如果没有可用的空闲线程,则创建一个新线程。newFixedThreadPool方法构建一个具有固定大小的线程池。如果提交的任务数多于空闲的线程数,那么把得不到服务的任务放置到队列中。当其他任务完成以后再运行它们。newSingleThreadExecutor是一个退化了的大小为1的线程池:由一个线程执行提交的任务,一个接着一个。这3个方法返回实现了ExecutorService接口的ThreadPoolExecutor类的对象。
可用下面的方法之一将一个Runnable对象或Callable对象提交给ExecutorService: Future<?> submit(Runnable task)
Future submit(Runnable task,T result) Future submit(Ca11able task)
该池会在方便的时候尽早执行提交的任务。调用submit时,会得到一个Future对象,可用来查询该任务的状态。
第一个submit方法返回一个奇怪样子的Future<?>。可以使用这样一个对象来调用isDone、cancel或 isCancelled。但是,get方法在完成的时候只是简单地返回null。
第二个版本的Submit也提交一个Runnable,并且 Future的get方法在完成的时候返回指定的result 对象。
第三个版本的Submit提交一个Callable,并且返回的Future对象将在计算结果准备好的时候得到它。
当用完一个线程池的时候,调用shutdown。该方法启动该池的关闭序列。被关闭的执行器不再接受新的任务。当所有任务都完成以后,线程池中的线程死亡。另一种方法是调用shutdownNow-访池取消尚未开始的所右任务并试图中断正在运行的线程。下面总结了在使用连接池时应该做的事:
1)调用Executors类中静态的方法 newCachedThreadPool或newFixedThreadPoolo2)调用submit提交Runnable或 Callable对象。
3)如果想要取消一个任务,或如果提交Callable对象,那就要保存好返回的Future对象。
4)当不再提交任何任务时,调用shutdown。
package com.company.concurrency.threadpool;
import java.io.File;
import java.util.Scanner;
import java.util.concurrent.*;
public class ThreadPoolTest {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(System.in)) {
System.out.println("Enter Base Directory(e.g /opt/jdk1.8.0/src): ");
String directory = scanner.nextLine();
System.out.println("Enter Key Word(e.g volatile):");
String keyWord = scanner.nextLine();
ExecutorService pool = Executors.newCachedThreadPool();
MatchCounter counter = new MatchCounter(new File(directory), keyWord,pool);
Future<Integer> result = pool.submit(counter);
try {
System.out.println(result.get()+" matched files.");
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
pool.shutdown();
int largestPoolSize = ((ThreadPoolExecutor)pool).getLargestPoolSize();
System.out.println("LargestPoolSize = " + largestPoolSize);
}
}
}
package com.company.concurrency.threadpool;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
public class MatchCounter implements Callable<Integer> {
private final File directory;
private final String keyWord;
private final ExecutorService pool;
private int count;
public MatchCounter(File directory, String keyWord, ExecutorService pool) {
this.directory = directory;
this.keyWord = keyWord;
this.pool = pool;
}
@Override
public Integer call() throws Exception {
count = 0;
try {
File[] files = directory.listFiles();
List<Future<Integer>> results = new ArrayList<>();
assert files != null;
for (File file : files) {
if (file.isDirectory()) {
MatchCounter counter = new MatchCounter(file, keyWord, pool);
Future<Integer> result = pool.submit(counter);
results.add(result);
} else {
if (search(file)) count++;
}
for (Future<Integer> task : results) {
try {
count += task.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return count;
}
private boolean search(File file) {
try (Scanner scanner = new Scanner(file, "UTF-8")) {
boolean found = false;
while (!found && scanner.hasNext()) {
String line = scanner.nextLine();
if (line.contains(keyWord)) found = true;
}
return found;
} catch (FileNotFoundException e) {
return false;
}
}
}
-
预定执行
ScheduledExecutorService接口具有为预定执行(Scheduled Execution)或重复执行任务而设计的方法。它是一种允许使用线程池机制的java.util.Timer的泛化。Executors类的newScheduledThreadPool和newSingleThreadScheduledExecutor方法将返回实现了ScheduledExecutorService接口的对象。
可以预定Runnable或 Callable在初始的延迟之后只运行一次。也可以预定一个Runnable对象周期性地运行。详细内容见API文档。 -
控制任务组
你已经了解了如何将一个执行器服务作为线程池使用,以提高执行任务的效率。有时,使用执行器有更有实际意义的原因,控制一组相关任务。例如,可以在执行器中使用shutdownNow方法取消所有的任务。
invokeAny方法提交所有对象到一个Callable对象的集合中,并返回某个已经完成了的任务的结果。无法知道返回的究竟是哪个任务的结果,也许是最先完成的那个任务的结果。对于搜索问题,如果你愿意接受任何一种解决方案的话,你就可以使用这个方法。例如,假定你需要对一个大整数进行因数分解计算来解码RSA密码。可以提交很多任务,每一个任务使用不同范围内的数来进行分解。只要其中一个任务得到了答案,计算就可以停止了。invokeAll方法提交所有对象到一个 Callable对象的集合中,并返回一个Future对象的列表,代表所有任务的解决方案。当计算结果可获得时,可以像下面这样对结果进行处理:
List<Ca11able> tasks = . . .; List<Future> results = executor.invokeAll(tasks);
for (Future result : results)
processFurther(result.get());
这个方法的缺点是如果第一个任务恰巧花去了很多时间,则可能不得不进行等待。将结果按可获得的顺序保存起来更有实际意义。可以用ExecutorCompletionService来进行排列。
用常规的方法获得一个执行器。然后,构建一个 ExecutorCompletionService,提交任务给完成服务( completion service)。该服务管理Future对象的阻塞队列,其中包含已经提交的任务的执行结果(当这些结果成为可用时)。这样一来,相比前面的计算,一个更有效的组织形式如下:
ExecutorCompletionService service = new ExecutorCompletionServicec<> (executor);
for (Callable task : tasks)
service.submit(task);
for(int i = 0;i < tasks.size();i++)
processFurther(service.take().get());
-
Fork-Join框架
package com.company.concurrency.forkjoin; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveTask; import java.util.function.DoublePredicate; public class ForkJoinTest { public static void main(String[] args) { final int SIZE = 10000000; double[] numbers = new double[SIZE]; for (int i = 0; i < SIZE; i++) { numbers[i] = Math.random(); } Counter counter = new Counter(numbers, 0, numbers.length, x -> x > 0.5); ForkJoinPool pool = new ForkJoinPool(); pool.invoke(counter); System.out.println(counter.join()); } private static class Counter extends RecursiveTask<Integer> { public static final int THRESHOLD = 1000; private final double[] numbers; private final int beginIndex; private final int endIndex; private final DoublePredicate filter; public Counter(double[] numbers, int beginIndex, int endIndex, DoublePredicate filter) { this.numbers = numbers; this.beginIndex = beginIndex; this.endIndex = endIndex; this.filter = filter; } @Override protected Integer compute() { if (endIndex - beginIndex < THRESHOLD) { int count = 0; for (int i = beginIndex; i < endIndex; i++) if (filter.test(numbers[i])) count++; return count; } else { int middle = (beginIndex + endIndex) / 2; Counter first = new Counter(numbers, beginIndex, middle, filter); Counter second = new Counter(numbers, middle, endIndex, filter); invokeAll(first, second); return first.join() + second.join(); } } } }
-
可完成future(CompletableFuture)
public class CompletableFuture<T> extends Object implements Future<T>, CompletionStage<T>
A
Future
that may be explicitly completed (setting its value and status), and may be used as aCompletionStage
, supporting dependent functions and actions that trigger upon its completion.When two or more threads attempt to
complete
,completeExceptionally
, orcancel
a CompletableFuture, only one of them succeeds.
为CompletableFuture对象增加一个动作
方法 | 参数 | 描述 |
---|---|---|
CompletableFuture thenApply | Function<? super T,? extends U> fn | 对结果应用一个函数 |
CompletableFuture thenCompose | Function<? super T,? extends CompletionStage> fn | 对结果调用函数并执行返回的future |
CompletableFuture handle | BiFunction<? super T,Throwable,? extends U> fn | 处理结果或错误 |
CompletableFuture thenAccept | Consumer<? super T> action | 类似于thenApply,不过结果为void |
CompletableFuture whenComplete | BiConsumer<? super T,? super Throwable> action | 类似于handle,不过结果为void |
CompletableFuture thenRun | Runnable action | 执行Runnable,结果为void |
10. 同步器
同步器
类 | 能做什么 | 说明 |
---|---|---|
CyclicBarrier | 允许线程集等待直至其中预定数目的线程达到一个公共障栅(barrier),然后可以选择执行一个处理障栅的动作 | 当大量的线程需要在它们的结果可用之前完成时 |
Phaser | 类似于循环障栅,不过有一个可变的计数 | Java SE7中引入 |
CountDownLatch | 允许线程等待直到计数为0 | 当一个或多个线程需要等待直到指定数目的时间发生 |
Exchanger | 允许两个线程在要交换的对象准备好时交换对象 | 当两个线程工作在统一数据结构的两个示例上的时候,一个向实例添加数据,另一个从实例删除数据 |
Semaphore | 允许线程等待直到被允许继续运行 | 限制访问资源的线程总数。如果许可数是1,尝尝阻塞线程直到另一个线程给出许可为止 |
SynchronousQueue | 允许一个线程把对象交给另一个线程 | 在没有显式同步的情况下,当两个线程准备好将一个对象将一个对象从一个线程传递到另一个时 |
-
信号量
概念上讲,一个信号量管理许多的许可证( permit)。为了通过信号量,线程通过调用acquire请求许可。其实没有实际的许可对象,信号量仅维护一个计数。许可的数目是固定的,由此限制了通过的线程数量。其他线程可以通过调用release释放许可。而且,许可不是必须由获取它的线程释放。事实上,任何线程都可以释放任意数目的许可,这可能会增加许可数目以至于超出初始数目。
-
倒计时门栓
一个倒计时门栓(CountDownLatch)让一个线程集等待直到计数变为0。倒计时门栓是一次性的。一旦计数为0,就不能再重用了。
一个有用的特例是计数值为1的门栓。实现一个只能通过一次的门。线程在门外等候直到另一个线程将计数器值置为0。
举例来讲,假定一个线程集需要一些初始的数据来完成工作。工作器线程被启动并在门外等候。另一个线程准备数据。当数据准备好的时候,调用countDown,所有工作器线程就可以继续运行了。
然后,可以使用第二个门栓检查什么时候所有工作器线程完成工作。用线程数初始化门栓。每个工作器线程在结束前将门栓计数减1。另一个获取工作结果的线程在门外等待,一旦所有工作器线程终止该线程继续运行。 -
障栅
CyclicBarrier类实现了一个集结点( rendezvous)称为障栅( barrier)。考虑大量线程运行在一次计算的不同部分的情形。当所有部分都准备好时,需要把结果组合在一起。当一个线程完成了它的那部分任务后,我们让它运行到障栅处。一旦所有的线程都到达了这个障栅,障栅就撤销,线程就可以继续运行。
下面是其细节。首先,构造一个障栅,并给出参与的线程数: CyclicBarrier barrier = new cyclicBarrier(nthreads);
每一个线程做一些工作,完成后在障栅上调用await :
public void run(){ dowork();
barrier.await(); }
await方法有一个可选的超时参数:
barrier.await(100,TimeUnit.MILLISECONDS);
如果任何一个在障栅上等待的线程离开了障栅,那么障栅就被破坏了(线程可能离开是因为它调用await时设置了超时,或者因为它被中断了)。在这种情况下,所有其他线程的await方法抛出 BrokenBarrierException 异常。那些已经在等待的线程立即终止 await的调用。
可以提供一个可选的障栅动作( barrier action),当所有线程到达障栅的时候就会执行这动作。 Runnable barrierAction = …;
CyclicBarrier barrier = new CyclicBarrier(nthreads, barrierAction);
该动作可以收集那些单个线程的运行结果。
障栅被称为是循环的(cyclic),因为可以在所有等待线程被释放后被重用。在这一点上,有别于CountDownLatch,CountDownLatch 只能被使用一次。
Phaser类增加了更大的灵活性,允许改变不同阶段中参与线程的个数。 -
交换器
当两个线程在同一个数据缓冲区的两个实例上工作的时候,就可以使用交换器(Exchanger)。典型的情况是,一个线程向缓冲区填入数据,另一个线程消耗这些数据。当它们都完成以后,相互交换缓冲区。
-
同步队列
同步队列是一种将生产者与消费者线程配对的机制。当一个线程调用SynchronousQueue的put方法时,它会阻塞直到另一个线程调用take方法为止,反之亦然。与Exchanger的情况不同,数据仅仅沿一个方向传递,从生产者到消费者。
即使SynchronousQueue类实现了BlockingQueue接口,概念上讲,它依然不是一个队列。它没有包含任何元素,它的size方法总是返回0。
11. 线程与Swing
-
运行耗时的任务
将线程与Swing一起使用时,必须遵循两个简单的原则。
uture> results = executor.invokeAll(tasks); for (Future result : results)
processFurther(result.get());
这个方法的缺点是如果第一个任务恰巧花去了很多时间,则可能不得不进行等待。将结果按可获得的顺序保存起来更有实际意义。可以用ExecutorCompletionService来进行排列。
用常规的方法获得一个执行器。然后,构建一个 ExecutorCompletionService,提交任务给完成服务( completion service)。该服务管理Future对象的阻塞队列,其中包含已经提交的任务的执行结果(当这些结果成为可用时)。这样一来,相比前面的计算,一个更有效的组织形式如下:
ExecutorCompletionService service = new ExecutorCompletionServicec<> (executor);
for (Callable task : tasks)
service.submit(task);
for(int i = 0;i < tasks.size();i++)
processFurther(service.take().get());
-
Fork-Join框架
package com.company.concurrency.forkjoin; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.RecursiveTask; import java.util.function.DoublePredicate; public class ForkJoinTest { public static void main(String[] args) { final int SIZE = 10000000; double[] numbers = new double[SIZE]; for (int i = 0; i < SIZE; i++) { numbers[i] = Math.random(); } Counter counter = new Counter(numbers, 0, numbers.length, x -> x > 0.5); ForkJoinPool pool = new ForkJoinPool(); pool.invoke(counter); System.out.println(counter.join()); } private static class Counter extends RecursiveTask<Integer> { public static final int THRESHOLD = 1000; private final double[] numbers; private final int beginIndex; private final int endIndex; private final DoublePredicate filter; public Counter(double[] numbers, int beginIndex, int endIndex, DoublePredicate filter) { this.numbers = numbers; this.beginIndex = beginIndex; this.endIndex = endIndex; this.filter = filter; } @Override protected Integer compute() { if (endIndex - beginIndex < THRESHOLD) { int count = 0; for (int i = beginIndex; i < endIndex; i++) if (filter.test(numbers[i])) count++; return count; } else { int middle = (beginIndex + endIndex) / 2; Counter first = new Counter(numbers, beginIndex, middle, filter); Counter second = new Counter(numbers, middle, endIndex, filter); invokeAll(first, second); return first.join() + second.join(); } } } }
-
可完成future(CompletableFuture)
public class CompletableFuture<T> extends Object implements Future<T>, CompletionStage<T>
A
Future
that may be explicitly completed (setting its value and status), and may be used as aCompletionStage
, supporting dependent functions and actions that trigger upon its completion.When two or more threads attempt to
complete
,completeExceptionally
, orcancel
a CompletableFuture, only one of them succeeds.
为CompletableFuture对象增加一个动作
方法 | 参数 | 描述 |
---|---|---|
CompletableFuture thenApply | Function<? super T,? extends U> fn | 对结果应用一个函数 |
CompletableFuture thenCompose | Function<? super T,? extends CompletionStage> fn | 对结果调用函数并执行返回的future |
CompletableFuture handle | BiFunction<? super T,Throwable,? extends U> fn | 处理结果或错误 |
CompletableFuture thenAccept | Consumer<? super T> action | 类似于thenApply,不过结果为void |
CompletableFuture whenComplete | BiConsumer<? super T,? super Throwable> action | 类似于handle,不过结果为void |
CompletableFuture thenRun | Runnable action | 执行Runnable,结果为void |
10. 同步器
同步器
类 | 能做什么 | 说明 |
---|---|---|
CyclicBarrier | 允许线程集等待直至其中预定数目的线程达到一个公共障栅(barrier),然后可以选择执行一个处理障栅的动作 | 当大量的线程需要在它们的结果可用之前完成时 |
Phaser | 类似于循环障栅,不过有一个可变的计数 | Java SE7中引入 |
CountDownLatch | 允许线程等待直到计数为0 | 当一个或多个线程需要等待直到指定数目的时间发生 |
Exchanger | 允许两个线程在要交换的对象准备好时交换对象 | 当两个线程工作在统一数据结构的两个示例上的时候,一个向实例添加数据,另一个从实例删除数据 |
Semaphore | 允许线程等待直到被允许继续运行 | 限制访问资源的线程总数。如果许可数是1,尝尝阻塞线程直到另一个线程给出许可为止 |
SynchronousQueue | 允许一个线程把对象交给另一个线程 | 在没有显式同步的情况下,当两个线程准备好将一个对象将一个对象从一个线程传递到另一个时 |
-
信号量
概念上讲,一个信号量管理许多的许可证( permit)。为了通过信号量,线程通过调用acquire请求许可。其实没有实际的许可对象,信号量仅维护一个计数。许可的数目是固定的,由此限制了通过的线程数量。其他线程可以通过调用release释放许可。而且,许可不是必须由获取它的线程释放。事实上,任何线程都可以释放任意数目的许可,这可能会增加许可数目以至于超出初始数目。
-
倒计时门栓
一个倒计时门栓(CountDownLatch)让一个线程集等待直到计数变为0。倒计时门栓是一次性的。一旦计数为0,就不能再重用了。
一个有用的特例是计数值为1的门栓。实现一个只能通过一次的门。线程在门外等候直到另一个线程将计数器值置为0。
举例来讲,假定一个线程集需要一些初始的数据来完成工作。工作器线程被启动并在门外等候。另一个线程准备数据。当数据准备好的时候,调用countDown,所有工作器线程就可以继续运行了。
然后,可以使用第二个门栓检查什么时候所有工作器线程完成工作。用线程数初始化门栓。每个工作器线程在结束前将门栓计数减1。另一个获取工作结果的线程在门外等待,一旦所有工作器线程终止该线程继续运行。 -
障栅
CyclicBarrier类实现了一个集结点( rendezvous)称为障栅( barrier)。考虑大量线程运行在一次计算的不同部分的情形。当所有部分都准备好时,需要把结果组合在一起。当一个线程完成了它的那部分任务后,我们让它运行到障栅处。一旦所有的线程都到达了这个障栅,障栅就撤销,线程就可以继续运行。
下面是其细节。首先,构造一个障栅,并给出参与的线程数: CyclicBarrier barrier = new cyclicBarrier(nthreads);
每一个线程做一些工作,完成后在障栅上调用await :
public void run(){ dowork();
barrier.await(); }
await方法有一个可选的超时参数:
barrier.await(100,TimeUnit.MILLISECONDS);
如果任何一个在障栅上等待的线程离开了障栅,那么障栅就被破坏了(线程可能离开是因为它调用await时设置了超时,或者因为它被中断了)。在这种情况下,所有其他线程的await方法抛出 BrokenBarrierException 异常。那些已经在等待的线程立即终止 await的调用。
可以提供一个可选的障栅动作( barrier action),当所有线程到达障栅的时候就会执行这动作。 Runnable barrierAction = …;
CyclicBarrier barrier = new CyclicBarrier(nthreads, barrierAction);
该动作可以收集那些单个线程的运行结果。
障栅被称为是循环的(cyclic),因为可以在所有等待线程被释放后被重用。在这一点上,有别于CountDownLatch,CountDownLatch 只能被使用一次。
Phaser类增加了更大的灵活性,允许改变不同阶段中参与线程的个数。 -
交换器
当两个线程在同一个数据缓冲区的两个实例上工作的时候,就可以使用交换器(Exchanger)。典型的情况是,一个线程向缓冲区填入数据,另一个线程消耗这些数据。当它们都完成以后,相互交换缓冲区。
-
同步队列
同步队列是一种将生产者与消费者线程配对的机制。当一个线程调用SynchronousQueue的put方法时,它会阻塞直到另一个线程调用take方法为止,反之亦然。与Exchanger的情况不同,数据仅仅沿一个方向传递,从生产者到消费者。
即使SynchronousQueue类实现了BlockingQueue接口,概念上讲,它依然不是一个队列。它没有包含任何元素,它的size方法总是返回0。
11. 线程与Swing
-
运行耗时的任务
将线程与Swing一起使用时,必须遵循两个简单的原则。
(1)如果一个动作需要花费很长时间,在一个独立的工作器线程中做这件事不要在事件分配线程中做