[关闭]
@king 2015-02-03T02:10:15.000000Z 字数 172562 阅读 4463

Java学习笔记

Java

相关网址收集

想下载的书籍

技术网站

工具

问题集

下载

学习网站


用notepad++打造轻量级Java开发环境

  1. cmd /k javac -encoding utf-8 "$(FULL_CURRENT_PATH)" & java -classpath "$(CURRENT_DIRECTORY)" "$(NAME_PART)" & PAUSE & EXIT

基础概念

JDK

版本简介

  • JDK(Java Development Kit) 是 Java 语言的软件开发工具包(SDK)。
  • SE(J2SE),standard edition,标准版,是我们通常用的一个版本,从JDK 5.0开始,改名为Java SE。
  • EE(J2EE),enterprise edition,企业版,使用这种JDK开发J2EE应用程序,从JDK 5.0开始,改名为Java EE。
  • ME(J2ME),micro edition,主要用于移动设备、嵌入式设备上的java应用程序,从JDK 5.0开始,改名为Java ME。
  • 没有JDK的话,无法编译Java程序,如果想只运行Java程序,要确保已安装相应的JRE。

组成

JDK包含的基本组件包括:

  • javac – 编译器,将源程序转成字节码
  • jar – 打包工具,将相关的类文件打包成一个文件
  • javadoc – 文档生成器,从源码注释中提取文档
  • jdb – debugger,查错工具
  • java – 运行编译后的java程序(.class后缀的)
  • appletviewer:小程序浏览器,一种执行HTML文件上的Java小程序的Java浏览器。
  • Javah:产生可以调用Java过程的C过程,或建立能被Java程序调用的C过程的头文件。
  • Javap:Java反汇编器,显示编译类文件中的可访问功能和数据,同时显示字节代码含义。
  • Jconsole: Java进行系统调试和监控的工具

常用的包

  • java.lang: 这个是系统的基础类,比如String等都是这里面的,这个包是唯一一个可以不用引入(import)就可以使用的包。
  • java.io: 这里面是所有输入输出有关的类,比如文件操作等。
  • java.nio:为了完善io包中的功能,提高io包中性能而写的一个新包 ,例如NIO非堵塞应用
  • java.net: 这里面是与网络有关的类,比如URL,URLConnection等。
  • java.util: 这个是系统辅助类,特别是集合类Collection,List,Map等。
  • java.sql: 这个是数据库操作的类,Connection, Statement,ResultSet等。
  • javax.servlet:这个是JSP,Servlet等使用到的类。

环境配置

以下假设安装于C:\Java\jdk1.7.0_67

  • JAVA_HOME:C:\Java\jdk1.7.0_67
  • CLASSPATH:.;%JAVA_HOME%\lib
    系统变量->编辑->变量名:Path 在变量值的最前面加上:%JAVA_HOME%\bin;
  • 如果是Vista、Win7、Win8系统
    使用鼠标右击“计算机”->属性->左侧高级系统设置->高级->环境变量
    系统变量->新建->变量名:JAVA_HOME 变量值:c:\jdk1.6.0_21
    系统变量->新建->变量名:CLASSPATH 变量值:.;%JAVA_HOME%\lib
    系统变量->编辑->变量名:Path 在变量值的最前面加上:%JAVA_HOME%\bin;
    (CLASSPATH中有一英文句号“.”后跟一个分号,表示当前路径的意思)
    (使用命令行的方法设置环境变量,只会对当前窗口生效)
    (改Path变量时,不是删除原有的值而是添加新的路径)

初始化块

初始化块没有名字,也就没有标识,因此无法通过类、对象来调用初始化块,只在创建Java对象时隐式执行,而且在构造器之前执行
创建Java对象时,系统总是先调用该类里定义的初始化块,如果一个类里定义了2个普通初始化块,则按顺序执行。
初始化块的修饰符只能是static。

  1. public class Person
  2. {
  3. //下面定义一个初始化块
  4. {
  5. int a = 6;
  6. if (a > 4)
  7. {
  8. System.out.println("Person初始化块:局部变量a的值大于4");
  9. }
  10. System.out.println("Person的初始化块");
  11. }
  12. //定义第二个初始化块
  13. {
  14. System.out.println("Person的第二个初始化块");
  15. }
  16. //定义无参数的构造器
  17. public Person()
  18. {
  19. System.out.println("Person类的无参数构造器");
  20. }
  21. public static void main(String[] args)
  22. {
  23. new person();
  24. }
  25. }

上面程序的main方法只创建了一个Person对象,程序的输出如下:

Person初始化块:局部变量a的值大于4
Person的初始化块
Person的第二个初始化块
Person类的无参数构造器

当Java创建一个对象时,系统先为该对象的所有实例变量分配内存,接着程序开始对这些实例变量执行初始化,其初始化的顺序时:先执行初始化块或声明变量时指定的初始值,再执行构造器里指定的初始值。


静态初始化块

定义初始化块时用了static修饰符,则为静态初始化块,又称类初始化块,在类初始化阶段执行,而不是创建对象时才执行


变量

数据类型

boolean 与 char
boolean :true或false
char 16 bits

数值(带正负号)
byte 8 bits
short 16 bits
int 32 bits
long 64 bits

浮点数
float 32 bits
double 64 bits
变量的概念实际上来自于面向过程的编程语言。在Java中,所谓的变量实际上是基本类型 (premitive type)


包装类Wrapper Class

8种基本数据类型不支持面向对象的编程机制,基本数据类型的数据也不具备“对象”的特性:没有变量、方法可以被调用。包装类可使8种基本数据类型的变量当成Object类型变量使用。

基本数据类型 包装类
byte Byte
short Short
int Integer
long Long
char Character
float Float
double Double
boolean Boolean
  1. //把一个字符串转换成Boolean对象
  2. Boolean bObj = new Boolean("false");

当试图使用一个字符串来创建Boolean对象时,如果传入的字符串是'true',或是此字符串不同字母的大小写变化形式,如'True',都将创建true对应的Boolean对象;如果传入其他字符串,则会创建false对应的Boolean对象
如果希望获得包装类对象中包装的基本类型变量,则可以使用包装类提供的xxxValue()实例方法。

从JDK 1.5 之后提供了自动装箱Autoboxing和自动拆箱AutoUnboxing功能。

自动装箱是指可以把一个基本类型变量直接赋给对应的包装类变量,或者赋给Object变量(Object是所有类的父类,子类对象可以直接赋给父类变量)
自动拆箱则与之相反

  1. //直接把一个boolean类型变量赋给一个Object类型变量
  2. Object boo1Obj = true;
  3. if (boo1Obj instanceof Boolean )
  4. {
  5. //先把Object对象强制类型转换为Boolean类型,再赋给boolean变量
  6. boolean b = (Boolean)boo1Obj;
  7. }

包装类还可实现基本类型变量和字符串之间的转换。
把字符串类型的值转换为基本类型的值有两种方式:

  • 利用包装类提供的parseXxx(String s)静态方法(Character除外)
  • 利用包装类提供的Xxx(String s)构造器
    String类提供了多个重载valueOf()方法,用于将基本类型变量转换成字符串。
  1. String intStr = "123";
  2. //把一个特定字符串转换成int变量
  3. int it1 = Integer.parseInt(intStr);
  4. int it2 = new Integer(intStr);
  5. String floatStr = "4.56";
  6. //把一个特定字符串转换成float变量
  7. float ft1 = Float.parseFloat(floatStr);
  8. float ft2 = new Float(floatStr);
  9. //把一个浮点变量转换成String变量
  10. String ftStr = String.valueOf(2.345f);
  11. String dbStr = String.valueOf(3.344);
  12. //把一个boolean变量转换成String变量
  13. String boolStr = String.valueOf(true);
  14. //把基本类型变量转换成字条串更简单的方法
  15. String intStr = 5 + "";

Java 7 为所有的包装类提供了一个静态的compare(xxx val1, xxx val2)方法,这样开发者可以比较两个基本类型值的大小,包括两个boolean类型的值(true>false)


变量的初始化

基本类型的数据成员的默认初始值:
数值型: 0
布尔值: false
其他类型: null


变量的命名

  1. 名称必须以字母、下划线(_)或$符号开头,不能用数字开头。
  2. 除了第一个字符之外,后面就可以用数字。反正不要用在第一个字符就行。
  3. 只要符合上述两条规则,你就可以随意地命名,但还得要避开Java的保留字。

成员变量和局部变量的命名

多个有意义的单词连缀而成,第一个单词首字母小写,后面每个单词首字母大写


形参个数可变的方法

如果在定义方法时,在最后一个形参的类型后增加三点(...),则表明该形参可以接受多个参数值,多个参数值被当成数组传入。

  1. public class Varargs
  2. {
  3. //定义了形参个数可变的方法
  4. public static void test(int a, String... books)
  5. {
  6. //book被当成数组处理
  7. for(String tmp: books)
  8. {
  9. System.out.println(tmp);
  10. }
  11. System.out.println(a);
  12. }
  13. public static void main(String[] args)
  14. {
  15. test(5, "疯狂Java讲义", "轻量级JavaEE企业应用实战");//传给books参数的实参数值无须是一个数组
  16. }
  17. }

如果用数组形参来定义:

  1. public static void test(int a, String[] books);

那么调用test时必须传给该形参一个数组:

  1. test(23, new String[]{"疯狂Java讲义", "轻量级Java EE企业应用实战"});

数组

Java中有数组(array)。数组包含相同类型的多个数据。声明一个整数数组:int[] a;

在声明数组时,数组所需的空间并没有真正分配给数组。可以在声明的同时,用new来创建数组所需空间:
int[] a = new int[100];
这里创建了可以容纳100个整数的数组。相应的内存分配也完成了。

还可以在声明的同时,给数组赋值。数组的大小也同时确定。
int[] a = new int[] {1, 3, 5, 7, 9};

使用int[i]来调用数组的i下标元素。i从0开始。其他类型的数组与整数数组相似。


二维数组

  1. type[][] = new type[length][];
  2. type[][] = new type[length1][length2];

使用new type[length]初始化一维数组后,相当于定义了length个type类型的变量;类似的,使用new type[length][]初始化这个数组后,相当于定义了length个type[]类型的变量。


表达式

布林表达式

两个boolean值的与、或、非的逻辑关系

  1. true && false and
  2. (3 > 1) || (2 == 1) or
  3. !true not

位运算

对整数的二进制形式逐位进行逻辑运算,得到一个整数

  1. & and
  2. | or
  3. ^ xor
  4. ~ not
  5. 5 << 3 0b101 left shift 3 bits
  6. 6 >> 1 0b110 right shift 1 bit
  1. 还有下列在C中常见的运算符,我会在用到的时候进一步解释:
  2. m++ 变量m1
  3. n-- 变量n1
  4. condition ? x1 : x2 condition为一个boolean值。根据condition,取x1x2的值

控制结构

Java中控制结构(control flow)的语法与C类似。它们都使用{}来表达隶属关系。

选择 if

  1. if (conditon1) {
  2. statements;
  3. ...
  4. }
  5. else if (condition2) {
  6. statements;
  7. ...
  8. }
  9. else {
  10. statements;
  11. ...
  12. }

上面的condition是一个表示真假值的表达式。statements;是语句。


循环

1.while

  1. while (condition) {
  2. statements;
  3. }

2.do... while

  1. do {
  2. statements;
  3. } while(condition); // 注意结尾的【;】

3.for

  1. for (initial; condition; update) {
  2. statements;
  3. }
  4. 或者
  5. for(String name:nameArray){}//逐个运行数组或其他集合的元素

4.跳过或跳出循环

在循环中,可以使用

  1. break; // 跳出循环
  2. continue; // 直接进入下一环

4.foreach循环
循环遍历数组或集合

  1. for (type variableName:array|collection)
  2. {
  3. statements;
  4. }

封装与接口

封装(encapsulation)是计算机常见的术语,即保留有限的外部接口(interface),隐藏具体实施细节。比如在Linux架构,就可以看到Linux操作系统封装了底层硬件的具体细节,只保留了系统调用这一套接口。用户处在封装的外部,只能通过接口,进行所需的操作。提高易用性、安全性


对象成员的封装

Java通过三个关键字来控制对象的成员的外部可见性(visibility): public, private, protected
public: 该成员外部可见,即该成员为接口的一部分
private: 该成员外部不可见,只能用于内部使用,无法从外部访问。
(protected涉及继承的概念,放在以后说)

内部方法并不受封装的影响,外部方法只能调用public成员。

在Java的通常规范中,表达状态的数据成员(比如height)要设置成private。对数据成员的修改要通过接口提供的方法进行(比如getHeight()和growHeight())。这个规范起到了保护数据的作用。用户不能直接修改数据,必须通过相应的方法才能读取和写入数据。类的设计者可以在接口方法中加入数据的使用规范。


类的封装

在一个.java文件中,有且只能有一个类带有public关键字。


访问控制符

访问控制符 private default protected public
同一个类中
同一个包中
子类中
全局范围内

Java修饰符适用范围总表

修饰符 外部类/接口 成员属性 方法 构造器 初始化块 成员内部类 局部成员
public
protected
包访问控制符 不能使用任何控制符 不能使用任何控制符
private
abstract
final
static
strictfp
synchronized
native
transient
volatile

实施接口

extends,表示对父类的继承,可以实现父类,也可以调用父类初始化 this.parent()。而且会覆盖父类定义的变量或者函数。这样的好处就是:架构师定义好接口,让工程师实现就可以了。整个项目开发效率得到提高,且开发成本大大降低。
implements,实现父类,子类不可以覆盖父类的方法或者变量。即使子类定义与父类相同的变量或者函数,也会被父类取代掉。


interface

以杯子为例,定义一个杯子的接口:

  1. interface Cup {
  2. void addWater(int w);
  3. void drinkWater(int w);
  4. }

Cup这个interface中定义了两个方法的原型(stereotype): addWater()和drinkWater()。一个方法的原型规定了方法名,参数列表和返回类型。原型可以告诉外部如何使用这些方法。

在interface中,我们

不需要定义方法的主体
不需要说明方法的可见性

注意第二点,interface中的方法默认为public

我们用implements关键字来实施interface。一旦在类中实施了某个interface,必须在该类中定义interface的所有方法。类中的方法需要与interface中的方法原型相符。否则,Java将报错。在类中可以定义interface没有提及的其他public方法。

  1. class MusicCup implements Cup {
  2. public void addWater(int w){
  3. this.water = this.water + w;
  4. }
  5. public void drinkWater(int w){
  6. this.water = this.water - w;
  7. }
  8. public int waterContent(){
  9. return this.water;
  10. }
  11. private int water = 0;
  12. }

分离接口的意义

我们使用了interface,但这个interface并没有减少我们定义类时的工作量。我们依然要像之前一样,具体的编写类。我们甚至于要更加小心,不能违反了interface的规定。既然如此,我们为什么要使用interface呢?

事实上,interface就像是行业标准。一个工厂(类)可以采纳行业标准 (implement interface),也可以不采纳行业标准。但是,一个采纳了行业标准的产品将有下面的好处:
更高质量: 没有加水功能的杯子不符合标准。
更容易推广: 正如电脑上的USB接口一样,下游产品可以更容易衔接。


实施多个接口

  1. class MusicCup implements MusicPlayer, Cup

在一个正常的Java项目中,我们往往需要编写不止一个.java程序,最终的Java产品包括了所有的Java程序。因此,Java需要解决组织Java程序的问题。包(package)的目的就是为了更好的组织Java程序。


包的建立

在Java程序的开始加入package就可以了。包名全部小写

  1. package com.vamei.society;

表示该程序在com.vamei.society包中。com.vamei(vamei.com的反写)表示包作者的域名 (很可惜,这个域名已经被别人注册了,所以只起演示作用)。Java要求包要有域名前缀,以便区分不同作者。society为进一步的本地路径名。com.vamei.society共同构成了包的名字。

包为Java程序提供了一个命名空间(name space)。一个Java类的完整路径由它的包和类名共同构成,比如com.vamei.society.Human。相应的Human.java程序要放在com/vamei/society/下。类是由完整的路径识别的,所以不同的包中可以有同名的类,Java不会混淆。比如com.vamei.society.Human和com.vamei.creature.Human是两个不同的类。

我们之前说过,一个Java文件中只能有一个public的类,该类要与.java文件同名。一个类可以没有public关键字,它实际上也表示一种权限: 该类在它所在的包中可见。也就是说,包中的其他Java程序可以访问该类。这是Java中的默认访问权限。父包和子包之间不能直接访问。

同样,对象的成员也可以是默认权限(包中可见)。比如我们去掉getHeight()方法前面的public关键字。


包的调用

如果整个包(也就是com文件夹)位于当前的工作路径中,那么不需要特别的设置,就可以使用包了

  1. import com.vamei.society.*;
  2. public class TestAgain{
  3. public static void main(String[] args){
  4. Human aPerson = new Human(180);
  5. System.out.println(aPerson.getHeight());
  6. }
  7. }

import用于识别路径。利用import语句,我们可以引入相应路径下的类。*表示引入society文件夹下的所有类,不包括子文件夹

我们也可以提供类的完整的路径。这可以区分同名但不同路径的类,比如:

  1. public class TestAgain{
  2. public static void main(String[] args) {
  3. com.vamei.society.Human aPerson =
  4. new com.vamei.society.Human(180);
  5. System.out.println(aPerson.getHeight());
  6. }
  7. }

由于我们提供了完整的类路径,所以不需要使用import语句。

包管理的是.class文件。


Java的常用包

  • java.lang:包含了Java语言的核心类,如String、Math、System和Thread类等,使用这个包下的类无须import语句导入
  • java.util:包含Java的大量工具类/接口和集合框架类/接口,如Arrays和List、Set等。
  • java.net:包含了一些Java网络编程相关的类/接口。
  • java.io:包含了一些Java输入/输出轮种相关的类/接口。
  • java.text:包含了一些Java格式化相关的类。
  • java.sql:包含了Java进行JDBC数据库编程的相关类/接口。
  • java.awt:包含了抽象窗口工具集(Abstract Window Toolkits)的相关类/接口,这些类主要用于构建图形用户界面(GUI)程序
  • java.swing:包含了Swing图形用户界面编程的相关类/接口,可用于构建平台无关的GUI程序

静态导入

  • 使用import可以省略写包名
  • 使用import static可以连类名都省略

1.导入指定类的单个静态数据成员或方法

  1. //导入java.lang.System类的out静态数据成员
  2. import static java.lang.System.out;

2.导入类的全部静态数据成员或方法

  1. //导入java.lang.System类的全部静态数据成员、方法
  2. import static java.lang.System.*;

继承

继承(inheritance)是面向对象的重要概念。继承是除组合(composition)之外,提高代码重复可用性(reusibility)的另一种重要方式。我们在组合(composition)中看到,组合是重复调用对象的功能接口。继承可以重复利用已有的类的定义。


类的继承

我们用extends关键字表示继承:

  1. class Woman extends Human

通过继承,我们创建了一个新类,叫做衍生类(derived class)。被继承的类(Human)称为基类(base class)。衍生类以基类作为自己定义的基础,并补充基类中没有定义的giveBirth()方法。

构造器不能继承。

如果定义一个Java类时并未显式指定这个类的直接父类,则这个类默认扩展java.lang.Object类。所以所有的Java对象都可以调用java.lang.Object类的实例方法。


instanceof运算符

判断前面的对象是否是后面的类

  1. //判断myObject是否String类对象
  2. boolean judge1= myObject instanceof String;

方法覆盖override

修改基类成员的方法。比如,在衍生层,也就是定义Woman时,可以修改基类提供的breath()方法:

  1. class Woman extends Human{
  2. public Human giveBirth(){
  3. System.out.println("Give birth");
  4. return (new Human(20));
  5. }
  6. public void breath(){
  7. super.breath();
  8. System.out.println("su...");
  9. }
  10. }

如果父类方法的访问权限是private,则该方法对子类是隐藏的,其子类既无法访问也无法重写该方法。如果子类中定义了一个与父类private方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。


方法重载overload和方法重写override

*重载主要发生在同一个类的多个同名方法之间,参数列表不同
*重写发生在子类和父类的同名方法之间,参数列表相同
*父类方法和子类方法也可能发生重载


衍生层构造方法

我们要在衍生类的定义中定义与类同名的构造方法。在该构造方法中:

比如下面的程序中,Human类有一个构造方法:

  1. class Human{
  2. public Human(int h){
  3. this.height = h;
  4. }
  5. ……
  6. }

衍生类Woman类的定义及其构造方法:

  1. class Woman extends Human{
  2. public Woman(int h){
  3. super(h); // base class constructor
  4. System.out.println("Hello, Pandora!");
  5. }
  6. ……
  7. }

不管我们是否使用super调用来执行父类构造器的初始化代码,子类构造器总会先调用父类构造器一次。依此类推,创建任何Java对象,最先执行的总是java.lang.Object类的构造器。


组合

在一个新类的定义中使用其他对象。这就是组合(composition)。组合是在Java中实现程序复用(reusibility)的基本手段之一。
类与类继承:

  1. class Animal
  2. {
  3. private void beat()
  4. {
  5. System.out.println("心脏跳动...");
  6. }
  7. public void breath()
  8. {
  9. beat();
  10. System.out.println("吸一口气,吐一口气,呼吸中...");
  11. }
  12. }
  13. //继承Animal,直接复用父类的breath方法
  14. class Bird extends Animal
  15. {
  16. public void fly
  17. {
  18. System.out.println("我在天空自由地飞翔");
  19. }
  20. }
  21. public class Test
  22. {
  23. public static void main(String[] args)
  24. {
  25. Bird b = new Bird();
  26. b.breath();
  27. b.fly();
  28. }
  29. }

将上述程序改写为组合

  1. class Animal
  2. {
  3. private void beat()
  4. {
  5. System.out.println("心脏跳动...");
  6. }
  7. public void breath()
  8. {
  9. beat();
  10. System.out.println("吸一口气,吐一口气,呼吸中...");
  11. }
  12. }
  13. class Bird
  14. {
  15. //将原来的父类嵌入原来的子类,作为子类的一个组合成分
  16. private Animal a;
  17. public Bird(Animal a)
  18. {
  19. this.a = a;
  20. }
  21. //重新定义一个自己的breath方法
  22. public void breath()
  23. {
  24. //直接复用Animal提供的breath方法来实现Bird的breath方法
  25. a.breath();
  26. }
  27. public void fly
  28. {
  29. System.out.println("我在天空自由地飞翔");
  30. }
  31. }
  32. public class Test
  33. {
  34. public static void main(String[] args)
  35. {
  36. //此时需要显式创建被嵌入的对象
  37. Animal a1 = new Animal();
  38. Bird b = new Bird(a1);
  39. b.breath();
  40. b.fly();
  41. }
  42. }

类数据与类方法

static数据成员

有一些数据用于表述类的状态。比如Human类,我们可以用“人口”来表示Human类的对象的总数。“人口”直接描述类的状态,而不是某个对象。
类的所有对象共享“人口”数据。这样的数据被称为类数据成员(class field)。
在类定义中,我们利用static关键字,来声明类数据成员,比如:
如:

  1. class Human{
  2. public Human(int h){
  3. this.height = h;
  4. }
  5. public int getHeight() {
  6. return this.height;
  7. }
  8. public void growHeight(int h){
  9. this.height = this.height + h;
  10. }
  11. public void breath(){
  12. System.out.println("hu...hu...");
  13. }
  14. private int height;
  15. private static int population;
  16. public static boolean is_mammal = true;
  17. }

我们定义了两个类数据成员: population和is_mammal。所有Human对象都共享一个population数据;任意Human对象的is_mammal(是哺乳动物)的属性都为true。

类数据成员同样要设置访问权限。对于声明为public的类数据成员,可以利用class.field的方式或者object.field(如果存在该类的对象)的方式从外部直接访问。这两种访问方式都是合理的,因为类数据成员可以被认为是类的属性,可以认为是所有成员共享的属性。如果类数据成员被定义为private,那么该类数据成员只能从类的内部访问。

(上面将is_mammal设置成了public,只是为了演示。这样做是挺危险的,万一有人使用 Human.is_mammal=false;,所有人类都遭殃。还是那个基本原则,要尽量将数据设置为private。)


类方法(static方法)

我们也可以有类方法,也就是声明为static的方法。类方法代表了类可以实现的动作,其中的操作不涉及某个具体对象。如果一个方法声明为static,那么它只能调用static的数据和方法,而不能调用非static的数据和方法。

事实上,在static方法中,将没有隐式传递的this和super参数。我们无从引用属于对象的数据和方法(这正是我们想要的效果)。类方法中,不能访问对象的数据。

下面我们增加一个static方法getPopulation(),该方法返回static数据population:

  1. class Human{
  2. //……
  3. public static int getPopulation(){
  4. return Human.population;
  5. }
  6. //……
  7. }

调用类方法时,我们可以通过class.method()的方式调用,也可以通过object.method()的方式调用。比如使用下面的Test类测试:

  1. public class Test{
  2. public static void main(String[] args){
  3. System.out.println(Human.getPopulation());
  4. Human aPerson = new Human(160);
  5. System.out.println(aPerson.getPopulation());
  6. }
  7. }

我们通过两种方式,在类定义的外部调用了类方法getPopulation()。


对象方法修改类数据

我们看到,对象方法可以访问类数据。这是非常有用的概念。类的状态有可能随着对象而发生变化。比如“人口”,它应该随着一个对象的产生而增加1。我们可以在对象的方法中修改类的“人口”数据。我们下面在构造方法中访问类数据成员。这里的构造方法是非static的方法,即对象的方法:

  1. class Human{
  2. public Human(int h) {
  3. this.height = h;
  4. Human.populatin = Human.population + 1;//注意这里
  5. }
  6. //……
  7. private static int population;
  8. private static boolean is_mammal = true;
  9. }

当我们每创建一个对象时,都会通过该对象的构造方法修改类数据,为population类数据增加1。这样,population就能实时的反映属于该类的对象的总数

除了上面举的构造方法的例子,我们也可以在普通的对象方法中访问类数据。


final

final关键字的基本含义是: 这个数据/方法/类不能被改变了。

final基本类型的数据: 定值 (constant value),只能赋值一次,不能再被修改。
final方法: 该方法不能被覆盖。private的方法默认为final的方法。
final类: 该类不能被继承。

普通类型的对象也可以有final关键字,它表示对象引用(reference)不能再被修改。即该引用只能指向一个对象。但是,对象的内容可以改变 (类似于C中的static指针)。我们将在以后介绍对象引用。

如果一个基本类型的数据既为final,也是static,那么它是只存储了一份的定值。这非常适合于存储一些常量,比如圆周率。


接口的继承与抽象类

在实施接口中,我们利用interface语法,将interface从类定义中独立出来,构成一个主体。interface为类提供了接口规范。

在继承中,我们为了提高程序的可复用性,引入的继承机制。当时的继承是基于类的。interface接口同样可以继承,以拓展原interface。


接口继承

接口继承(inheritance)与类继承很类似,就是以被继承的interface为基础,增添新增的接口方法原型。比如,我们以Cup作为原interface:

  1. interface Cup {
  2. void addWater(int w);
  3. void drinkWater(int w);
  4. }

我们在继承Cup的基础上,定义一个新的有刻度的杯子的接口,MetricCup,接口如下:

  1. interface MetricCup extends Cup{
  2. int WaterContent();
  3. }

我们增添了一个新的方法原型WaterContent(),这个方法返回一个整数(水量)。


interface的多重继承

在Java类的继承中,一个衍生类只能有一个基类。也就是说,一个类不能同时继承多于一个的类。在Java中,interface可以同时继承多于一个interface,这叫做多重继承(multiple inheritance)。

比如我们有下面一个Player接口:

  1. interface Player{
  2. void play();
  3. }

我们新增一个MusicCup的接口。它既有Cup接口,又有Player接口,并增加一个display()方法原型。

  1. interface MusicCup extends Cup, Player {
  2. void display();
  3. }

(如何使用interface,见实施接口)


抽象类

在生活中,我们会有一些很抽象的概念。这些抽象的概念往往是许多类的集合,比如:

粮食 (可以是玉米、小麦、大米)
图形 (可以是三角形、圆形、正方形)
人类 (可以是男人、女人)

在组织这样的关系时,我们可以使用继承。根据我们的常识:

"Food类的对象"的说法是抽象的。这样一个对象应该是属于Corn, Rice, Wheat子类中的一个。
Food类有eat()方法 (食物可以吃)。然而,这样的一个动作是抽象的。粮食的具体吃法是不同的。比如Corn需要剥皮吃,Wheat要磨成面粉吃。我们需要在每个类中覆盖Food类的eat()方法。


抽象与具体

Java中提供了抽象类abstract class的语法,用于说明类及其方法的抽象性。比如

  1. abstract class Food {
  2. public abstract void eat();
  3. public void happyFood();
  4. {
  5. System.out.println("Good! Eat Me!");
  6. }
  7. }

类中的方法可以声明为abstract,比如上面的eat()。这时,我们不需要具体定义方法,只需要提供该方法的原型。这与接口类似。当我们在比如Corn类中继承该类时,需要提供eat()方法的具体定义。

当一个类中出现abstract方法时,这个类的声明必须加上abstract关键字,否则Java将报错。一个abstract类不能用于创建对象。


抽象类的继承

我们可以像继承类那样继承一个抽象类。我们必须用完整的方法定义,来覆盖抽象类中的抽象方法,否则,衍生类依然是一个抽象类。
抽象类的定义中可以有数据成员。数据成员的继承与正常类的继承相同。


对象引用

外部可以调用类来创建对象,比如:

  1. Human aPerson = new Human(160);

创建了一个Human类的对象aPerson。

上面是一个非常简单的表述,但我们有许多细节需要深入:

  1. 首先看等号的右侧。new是在内存中为对象开辟空间。具体来说,new是在内存的堆(heap)上为对象开辟空间。这一空间中,保存有对象的数据和方法
  2. 再看等号的左侧。aPerson指代一个Human对象,被称为对象引用(reference)。实际上,aPerson并不是对象本身,而是类似于一个指向对象的指针。aPerson存在于内存的栈(stack)中。
    当我们用等号赋值时,是将右侧new在堆中创建对象的地址赋予给对象引用。
  3. 这里的内存,指的是JVM (Java Virtual Machine)虚拟出来的Java进程内存空间。内存的堆和栈概念可参考Linux从程序到进程。

栈的读取速度比堆快,但栈上存储的数据受到有效范围的限制。在C语言中,当一次函数调用结束时,相应的栈帧(stack frame)要删除,栈帧上存储的参量和自动变量就消失了。Java的栈也受到同样的限制,当一次方法调用结束,该方法存储在栈上的数据将清空。在 Java中,所有的(普通)对象都储存在堆上。因此,new关键字的完整含义是,在堆上创建对象。

基本类型(primitive type)的对象,比如int, double,保存在栈上。当我们声明基本类型时,不需要new。一旦声明,Java将在栈上直接存储基本类型的数据。所以,基本类型的变量名表示的是数据本身,不是引用。


类型转换与多态

基本类型转换

Java可以对基本类型的变量进行类型转换。不同的基本类型有不同的长度和存储范围。如果我们从一个高精度类型转换到低精度类型,比如从float转换到int,那么我们有可能会损失信息。这样的转换叫做收缩变换(narrowing conversion)。这种情况下,我们需要显示的声明类型转换,比如:

  1. public class Test{
  2. public static void main(String[] args){
  3. int a;
  4. a = (int) 1.23; // narrowing conversion
  5. System.out.println(a);
  6. }
  7. }

如果我们从低精度类型转换成高精度类型,则不存在信息损失的顾虑。这样的变换叫做宽松变换(widening conversion)。我们不需要显示的要求类型转换,Java可以自动进行。


upcast与多态

将一个衍生类引用转换为其基类引用,这叫做向上转换(upcast)或者宽松转换。下面的BrokenCup类继承自Cup类,并覆盖了Cup类中原有的addWater()和drinkWater()方法:

  1. public class Test{
  2. public static void main(String[] args){
  3. Cup aCup;
  4. BrokenCup aBrokenCup = new BrokenCup();
  5. aCup = aBrokenCup; // upcast
  6. aCup.addWater(10); // method binding
  7. }
  8. }
  9. class Cup{
  10. public void addWater(int w){
  11. this.water = this.water + w;
  12. }
  13. public void drinkWater(int w){
  14. this.water = this.water - w;
  15. }
  16. private int water = 0;
  17. }
  18. class BrokenCup extends Cup{
  19. public void addWater(int w) {
  20. System.out.println("shit, broken cup");
  21. }
  22. public void drinkWater(int w) {
  23. System.out.println("om...num..., no water inside");
  24. }
  25. }

程序运行结果:

shit, broken cup

在上面可以看到,不需要任何显示说明,我们将衍生类引用aBrokenCup赋予给它的基类引用aCup。类型转换将由Java自动进行。

我们随后调用了aCup(我们声明它为Cup类型)的addWater()方法。尽管aCup是Cup类型的引用,它实际上调用的是BrokenCup的addWater()方法!也就是说,即使我们经过upcast,将引用的类型宽松为其基类,Java依然能正确的识别对象本身的类型,并调用正确的方法。Java可以根据当前状况,识别对象的真实类型,这叫做多态(polymorphism)。多态是面向对象的一个重要方面。

多态是Java的支持的一种机制,同时也是面向对象的一个重要概念。这提出了一个分类学的问题,既子类对象实际上“是”父类对象。比如一只鸟,也是一个动物;一辆汽车,也必然是一个交通工具。Java告诉我们,一个衍生类对象可以当做一个基类对象使用,而Java会正确的处理这种情况。

我们可以说用杯子(Cup)喝水(drinkWater)。实际上,喝水这个动作具体含义会在衍生类中发生很大变换。比如用吸管喝水,和从一个破杯子喝水,这两个动作差别会很大,虽然我们抽象中都讲“喝水”。我们当然可以针对每个衍生类分别编程,调用不同的drinkWater方法。然而,作为程序员,我们可以对杯子编程,调用Cup的drinkWater()方法,而无论这个杯子是什么样的衍生类杯子。Java会调用相应的正确方法,正如我们在上面程序中看到的。

看一个更加有意义的例子,我们给Human类增加一个drink()方法,这个方法接收一个杯子对象和一个整数作为参数。整数表示喝水的水量:

  1. public class Test{
  2. public static void main(String[] args){
  3. Human guest = new Human();
  4. BrokenCup hisCup = new BrokenCup();
  5. guest.drink(hisCup, 10);
  6. }
  7. }
  8. class Human{
  9. void drink(Cup aCup, int w){
  10. aCup.drinkWater(w);
  11. }
  12. }

程序运行结果:

shit, no water inside

我们在Human类的drink()的定义中,要求第一个参量为Cup类型的引用。但在实际运用时(Test类),将Cup的BrokenCup衍生类对象。这实际上是将hisCup向上转型称为Cup类,传递给drink()方法。在方法中,我们调用了drinkWater()方法。Java发现这个对象实际上是BrokenCup对象,所以实际调用了BrokenCup的相应方法。


downcast

我们可以将一个基类引用向下转型(downcast)成为衍生类的引用,但要求该基类引用所指向的对象,已经是所要downcast的衍生类对象。比如可以将上面的hisCup向上转型为Cup类引用后,再向下转型成为BrokenCup类引用。


Object类型

Java中,所有的类实际上都有一个共同的继承祖先,即Object类。Object类提供了一些方法,比如toString()。我们可以在自己的类定义中覆盖这些方法。


String类

String类包含在java.lang包中。这个包会在Java启动的时候自动import,所以可以当做一个内置类(built-in class)。我们不需要显式的使用import引入String类。


创建字符串

创建String类对象不需要new关键字。比如:

  1. public class Test{
  2. public static void main(String[] args){
  3. String s = "Hello World!";
  4. System.out.println(s);
  5. }
  6. }

实际上,当你写出一个"Hello World"表达式时,内存中就已经创建了该对象。如果使用new String("Hello World!"),会重复创建出一个字符串对象。String类是唯一一个不需要new关键字来创建对象的类。


字符串操作

可以用+实现字符串的连接(concatenate),比如:

  1. "abc" + s

字符串的操作大都通过字符串的相应方法实现,比如下面的方法:

  1. 方法 效果
  2. s.length() //返回s字符串长度
  3. s.charAt(2) //返回s字符串中下标为2的字符
  4. s.substring(0, 4) //返回s字符串中下标0到4的子字符串
  5. s.indexOf("Hello") //返回子字符串"Hello"的下标
  6. s.startsWith(" ") //判断s是否以空格开始
  7. s.endsWith("oo") //判断s是否以"oo"结束
  8. s.equals("Good World!") //判断s是否等于"Good World!" ,"=="只能判断字符串是否保存在同一位置。需要使用equals()判断字符串的内容是否相同。
  9. s.compareTo("Hello Nerd!") //比较s字符串与"Hello Nerd!"在词典中的顺序,返回一个整数,如果<0,说明s在"Hello Nerd!"之前;如果>0,说明s在"Hello Nerd!"之后;如果==0,说明s与"Hello Nerd!"相等。
  10. s.trim() //去掉s前后的空格字符串,并返回新的字符串
  11. s.toUpperCase() //将s转换为大写字母,并返回新的字符串
  12. s.toLowerCase() //将s转换为小写,并返回新的字符串
  13. s.replace("World", "Universe") //将"World"替换为"Universe",并返回新的字符串

不可变对象

String类对象是不可变对象(immutable object)。程序员不能对已有的不可变对象进行修改。我们自己也可以创建不可变对象,只要在接口中不提供修改数据的方法就可以。

然而,String类对象确实有编辑字符串的功能,比如replace()。这些编辑功能是通过创建一个新的对象来实现的,而不是对原有对象进行修改。比如:

  1. s = s.replace("World", "Universe");

右边对s.replace()的调用将创建一个新的字符串"Hello Universe!",并返回该对象(的引用)。通过赋值,引用s将指向该新的字符串。如果没有其他引用指向原有字符串"Hello World!",原字符串对象将被垃圾回收。


枚举类

  1. //创建枚举类
  2. public enum Gender{
  3. //此处的枚举值必须调用对应的构造器来创建,无须new关键字,也无须显式调用构造器
  4. Male("男"),FEMALE("女");
  5. private final String name;
  6. //枚举类的构造器只能用private修饰
  7. private Gender(String name){
  8. this.name = name;
  9. }
  10. public String getName(){
  11. return this.name;
  12. }
  13. }
  14. public class GenderTestP{
  15. public static void main(String[] args){
  16. //通过Enum的valueOf方法来获取指定枚举类的枚举值
  17. Gender g = Enum.valueOf(Gender.class , "FEMALE");
  18. }
  19. }

java.lang.Enum类中提供了如下几个方法:

  • int compareTo(E o):该方法用于与指定枚举对象比较顺序。只能与相同类型的枚举实例进行比较。
  • String name():返回此枚举实例的名称。
  • int ordinal():返回枚举值在枚举类中的索引值(即枚举值在枚举声明中的位置,第一个枚举值的索引值为零)
  • String toString():返回枚举常量的名称,与name方法相似但更常用
  • public static > T valueOf(class enumType,String name):返回指定枚举类中指定名称的枚举值

实现接口的枚举类

枚举类也可以一个或多个接口。

  1. public interface GenderDesc{
  2. void info();//抽象方法
  3. }
  4. public enum Gender implements GenderDesc{
  5. //其他部分与上面的Gender类完全相同
  6. ……
  7. //增加下面的info方法,实现GenderDesc接口必须实现的
  8. public void info(){
  9. System.out.println("输出详细情况");
  10. }
  11. }

如果需要每个枚举值在调用该方法时呈现出不同的行为方式,则可以使用匿名内部子类

  1. public enum Gender implements GenderDesc{
  2. //此处的枚举值必须调用对应的构造器创建
  3. //花括号部分实际上是一个类体部分
  4. MALE("男"){
  5. public void info(){
  6. System.out.println("男性");
  7. }
  8. }
  9. FEMALE("女"){
  10. public void info(){
  11. System.out.println("女性");
  12. }
  13. }
  14. //枚举类的其他部分与上面的Gender类完全相同
  15. ……
  16. }

非抽象的枚举类默认使用final修饰,但是上面的类继承了GenderDesc接口,包含了抽象方法,那么它是抽象枚举类,可以派生子类。


与运行环境交互

Scanner

Scanner获取键盘输入

Scanner主要提供了两个方法来扫描输入:

  • hasNextXxx():是否还有下一个输入项,其中Xxx可以是Int、Long等代表基本数据类型的字符串。如果需要判断是否包含下一个字符串,则可以省去Xxx。
  • nextXxx():获取下一个输入项。
    默认情况下,Scanner使用空白(包括空格、Tab空白、回车)作为多个输入项之间的分隔符。
  1. Scanner sc = new Scanner(System.in);//System.in代表标准输入,就是键盘输入
  2. //以下代码指定只把回车作为分隔符
  3. sc.useDelimiter("\n");//其中的参数是一个正则表达式

Scanner提供了两个简单的方法来逐行读取:

  • boolean hasNextLine():返回输入源中是否还有下一行。
  • String nextLine():返回输入源中下一行的字符串。

Scanner读取文件输入

创建Scanner对象时传入一个File对象作为参数

  1. //将一个File对象作为Scanner的构造器参数,Scanner读取文件内容
  2. Scanner sc = new Scanner(new File("ScannerFileTest.java"));
  3. //判断是否还有下一行
  4. while(sc.hasNextLine()){
  5. //输出文件的下一行
  6. System.out.println(sc.nextLine);
  7. }

BufferedReader

BufferedReader获取键盘输入

Scanner是Java 5 新增的工具类。在这之前程序常通过BufferedReader类来读取键盘输入。
BufferedReader是Java IO 流中的一个字符、包装流,它必须建立在另一个字符流的基础之上。但标准输入System.in是字节流,程序需要使用转换流InputStreamReader将其包装成字符流,所以程序中用于获取键盘输入的BufferedReader对象采用如下代码创建:

  1. //以System.in字节流为基础,创建一个BufferedReader对象
  2. BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
  3. //可以用readLine()方法来读取键盘输入
  4. String line = null;
  5. while((line = br.readLine()) != null){
  6. System.out.println("用户键盘输入是:" + line);
  7. }

使用BufferedReader可以逐行读取用户的键盘输入,每次键盘输入都被BufferedReader当成String对象,它不能读取基本类型输入项。


系统相关

Java程序在不同操作系统上运行时,可能需要取得平台相关的属性,或者调用平台命令来完成特定功能。Java提供了System类和Runtime类来与程序的运行平台进行交互。


System类

System类代表当前Java程序的运行平台,程序不能创建System类的对象,System类提供了一些类Field和类方法,允许直接通过System类来调用这些Field和方法。
System类提供了代表标准输入、标准输出和错误输出的类Field,并提供了一些静态方法用于访问环境变量、系统属性的方法,还提供了加载文件和动态链接库(loadLibrary..()方法)的方法。

  1. //获取系统的所有环境变量,须先导入java.util.Map
  2. Map<String,String> env = System.getenv();
  3. for (String name : env.keySet()){
  4. System.out.println(name + "--->" + env.get(name));
  5. }
  6. //获取指定环境变量的值
  7. System.out.println(System.getenv("JAVA_HOME"));
  8. //获取所有的系统属性,须先导入java.util.Properties
  9. Properties props = System.getProperties();
  10. //将所有的系统属性保存到props.txt文件中
  11. props.store(new FileOutputStream("props.txt"), "System Properties");
  12. //输出特定的系统特性
  13. System.out.println(System.getProperty("os.name"));
  14. //获取系统当前时间
  15. long time1 = System.currentTimeMillis();//返回与1970年1月1日00:00:00的时间差,毫秒为单位
  16. long time2 = System.currentTimeMillis();//返回与1970年1月1日00:00:00的时间差,纳秒为单位
  17. //返回根据该对象的地址计算得到的精确hashCode值,若两对象的hashCode相同则为同一对象
  18. String s = new String("Hello");
  19. System.out.println(System.identityHashCode(s));

Runtime类

Runtime代表系统运行时环境,每个Java程序都有一个与之相对应的Runtime实例,应用程序通过该对象与其运行时环境相连。应用程序不能创建自己的Runtime实例,但可以通过getRuntime()方法获取与之相关联的Runtime对象。

  1. //获取Java程序关联的运行时对象
  2. Runtime rt = Runtime.getRuntime();
  3. System.out.println("处理器数量:" + rt.availableProcessors());
  4. System.out.println("空间内存数:" + rt.freeMemory());
  5. System.out.println("总内存数:" + rt.totalMemory());
  6. System.out.println("可用最大内存数:" + rt.maxMemory());
  7. //单独启动一个进程来运行操作系统命令
  8. //运行记事本程序,必须加throws Exception语句,否则编译不通过
  9. public static void main(String[] args) throws Exception{
  10. Runtime.getRuntime().exec("notepad");
  11. }

常用类


Object类

  • boolean equals(Object obj):判断指定对象与该对象是否相等,此处相等的标准是两个对象是同一对象,因此该equals()方法通常没有太大实用价值。
  • protected void finalize():当系统中没有引用变量引用到该对象时,垃圾回收器调用此方法来清理该对象的资料。
  • Class getClass():返回该对象的运行时类;
  • int hashCode():返回该对象的hashCode值。默认情况下,Object类的hashCode()方法根据该对象地址来计算(即与System.indentityHashCode(Object x)方法计算结果相同)。但很多类都重写了Object类的hashCode方法,不再根据地址来计算其hashCode()方法值。
  • String toString():返回该对象的字符串表示,当我们使用System.out.println()方法输出一个对象,或把某个对象和字条串进行连接运算时,系统会自动调用该对象的toString()方法返回该对象的字符串表示。Object类的toString()方法返回“运行时类名@十六进制的hashCode值“格式的字条串,但很多类都重写了该方法。

Java还提供了一个protected修饰的clone()方法,用于帮助其他对象实现“自我克隆”,即得到当前对象的一个副本,二者之间完全隔离。
步骤如下:

  • 自定义类实现Cloneable接口。这是一个标记性的接口,实现该接口的对象可以实现“自我克隆”,接口里没有定义任何方法。
  • 自定义类实现自己的clone()方法。
  • 实现clone()方法时通过super.clone();调用Object实现的clone()方法来得到该对象的副本,并返回该副本。
  1. class Address{
  2. String detail;
  3. public Address(String detail){
  4. this.detail = detail;
  5. }
  6. }
  7. //实现Cloneable接口
  8. class User implements Cloneable{
  9. int age;
  10. Address address;
  11. public User(int age){
  12. this.age = age;
  13. address = new Address("广州天河");
  14. }
  15. //通过调用super.clone()来实现clone()方法,User是返回类型,返回一个User对象
  16. public User clone()
  17. throws CloneNotSupportedException
  18. {
  19. return (User)super.clone();
  20. }
  21. }
  22. public class CloneTest{
  23. public static void main(String args[]) throws CloneNotSupportedException{
  24. User u1 = new User(29);
  25. //clone得到u1对象的副本
  26. User u2 = u1.clone();
  27. System.out.println(u1 == u2);//false
  28. //clone只是简单复制引用变量,u1和u2的address都指向同一个字符串,所以下面输出true
  29. //如果需要把引用变量指向的对象也克隆,则要自己开发者自己进行“递归”克隆
  30. System.out.println(u1.address == u2.address);
  31. }
  32. }

Objects工具类

注意不是Object!
Java为工具类命名习惯是添加一个字母s,比如操作数组的工具类是Arrays,操作集合的工具类是Collections。

  1. public class ObjTest{
  2. //定义一个obj变量,它的默认值是null;
  3. static ObjTest obj;
  4. public static void main(String args []){
  5. //输出一个null对象的hashCode值,输出0
  6. System.out.println(Objects.hashCode(obj));
  7. //输出一个null对象的toString,输出null
  8. System.out.println(Objects.toString(obj));
  9. //要求obj不能为null,如果obj为null则引发异常,
  10. System.out.println(Objects.requireNonNull(obj,"obj参数不能是null!"));
  11. }
  12. //Objects.requireNonNull()方法主要用于对方法形参进行输入校验,当参数不为null时返回参数本身,否则引发异常
  13. public Foo(Bar bar){
  14. //校验bar参数,如果bar参数为null将引发异常,否则this.bar被赋值为bar参数
  15. }
  16. }

String、StringBuffer和StringBuilder类

String类是不可变类,字符序列不可改变。
StringBuffer对象则代表一个字符序列可变的字符串,通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()等方法改变字条串对象的字符序列,一旦通过StringBuffer生成了最终想要的字符串,就可以调用它的toString()方法将其转换为一个String对象。
StringBuilder类与StringBuffer相似,但后者线程安全而前者未实现,故优先考虑用StringBuilder类来创建内容可变的字符串。
String类提供了大量构造器:

  • String():创建一个包含0个字符串序列的String对象,并不是返回null。
  • String(byte[] bytes, Charset charset):使用指定的字符集将指定的byte[]数组解码成一个新的String对象
  • String(byte[] bytes, int offset,int length,String charsetName):使用指定的字符集将指定的byte[]数组从offset开始、长度为length的子数组解码成一个新的String对象。
  • String(byte[] bytes, String charsetName):使用指定的字符集将指定的byte[]数组解码成一个新的String对象。
  • String(char[] value, int offset, int count):将指定的字符数组从offset开始、长度为count的字符元素连缀成字符串。
  • String(String original):根据字符串直接量来创建一个String对象。也就是说,新创建的String对象是该参数字符串的副本。
  • String(StringBuffer buffer):根据StringBuffer对象来创建对应的String对象。
  • String(StringBuilder builder):根据StringBuilder对象来创建对应的String对象。

String类也提供了大量方法来操作字符串对象
详见API


ThreadLocalRandom与Random

Random类有两个构造器,一个使用默认种子(当前时间),另一个需要显式传入一个long整数。
ThreadLocalRandom类在并发访问的环境下,可以减少多线程竞争,最终保证系统具有良好的性能。
ThreadLocalRandom类的用法与Random类的用法基本相似,它提供了一个静态的current()方法来获取ThreadLocalRandom对象,之后可以调用各种nextXxx()方法来获取伪随机数了。

  1. import java.util.random;
  2. Random rand = new Random();
  3. //生成随机布尔变量
  4. boolean b = rand.nextBoolean();
  5. //生成随机byte数组
  6. byte[] b = new byte[16];
  7. rand.nextBytes(b);
  8. //生成0.0~1.0之间的伪随机double数
  9. double b = rand.nextDouble();
  10. //生成平均值是0.0,标准差是1.0的伪高斯数
  11. double b = rand.nextGaussian();
  12. //生成一个处于int整数聚范围的伪随机整数
  13. int b = rand.nextInt();
  14. //生成0~26之间的伪随机整数
  15. int b = rand.nextInt(26);
  16. //使用ThreadLocalRandom
  17. ThreadLocalRandom rand = ThreadLocalRandom.current();
  18. //生成4~20之间的伪随机整数
  19. int b = rand.nextInt(4,20);

国际化与格式化

国际化Internationalization,简称I18N。
本地化Localization简称L10N。

Java程序国际化的思路是将程序中的标签、提示等信息放在资源文件中,程序需要支持哪些国家、语言环境,就对应提供相应的资源文件。资源文件是key-value对,每个资源文件中的key是不变的,但value则随不同的国家、语言而改变


Java国际化的思路

java程序的国际化主要通过如下三个类完成:

  • java.util.ResourceBundle:用于加载国家、语言资源包
  • java.util.Locale:用于封装特定的国家/区域、语言环境
  • java.text.MessageFormat:用于格式化带占位符的字符串

资源文件的命名可以有如下三种形式:

  • baseName_language_country.properties
  • baseName_language.properties
  • baseName.properties
    baseName用户随意指定;language和country都不可随意变化,必须是Java所支持的语言和国家

Java支持的国家和语言

调用Locale类的getAvailableLocales()方法,返回一个Locale数组,包含了Java所支持的国家和语言。

  1. import java.util.Locale;
  2. //返回Java所支持的全部国家和语言的数组
  3. Locale[] localeList = Locale.getAvailableLocales();
  4. //遍历并输出
  5. for(int i = 0; i < localeList.length; i++){
  6. System.out.println(localeList[i].getDisplayCountry() + "=" + localeList[i].getCountry() + " " + localeList[i].getDisplayLanguage() + "=" + localeList[i].getLanguage());
  7. }

Java集合

为了保存数量不确定的数据,以及保存具有映射关系的数据(也被称为关联数组),Java提供了集合类。集合类主要负责保存、盛装其他数据,因此集合类又被称为容器类。所有集合类都处于java.util包下。后来为了处理多线程环境下的并发安全问题,Java 5 还在java.util.concurrent包下提供了一些多线程支持的集合类。
集合只能保存对象。数组可以保存基本类型+对象。
Set集合无序,元素不能重复。
List集合有序,元素可以重复,像一个长度可变的数组。
Map集合每项数据都由两个值组成,key不可重复,value可以重复。


Collection和Iterator接口

Collection接口是List、Set、和Queue接口的父接口,故其方法均可用于三个集合。以下为部分清单,详见API

  • boolean add(Object o):向集合里添加一个元素,若集合对象被添加操作改变则返回true。
  • boolean addAll(Collection c):该方法把集合c里的所有元素添加到指定集合。若成功则返回true。
  • void clear():清除集合里的所有元素,使集合长度变为0。
  • boolean contains(Object o):返回集合里是否包含指定元素。
  • boolean containsAll(Collection c):返回集合里是否包含集合c里的所有元素。
  • boolean isEmpty():返回集合是否为空。
  • Iterator iterator():返回一个Iterator对象,用于遍历集合里的元素。
  • boolean remove(Object o):删除集合中的指定元素o,当集合中包含了一个或多个元素o时,这些元素将被删除,该方法将返回true
  • boolean removeAll(Collection c):从集合中删除集合c里包含的所有元素,删除了一个或一个以上的元素则返回true
  • boolean retainAll(Collection c):从集合中删除集合c里不包含的元素(相当于变成两个集合的交集),若改变了调用方法的集合,则返回true。
  • int size(): 该方法返回集合里元素的个数
  • Object[] toArray():把集合转换成一个数组,所有的集合元素变成对应的数组元素。
  1. import java.util.Collection;
  2. Collection c = new ArrayList();
  3. //添加元素
  4. c.add("孙悟空");
  5. //虽然集合不能放基本类型的值,但Java支持自动装箱
  6. c.add(6);
  7. //删除指定元素
  8. c.remove(6);
  9. //判断是否包含指定字符串
  10. boolean b = c.contains("孙悟空");

使用Iterator接口遍历集合元素

Iterator主要用于遍历(即迭代访问)Collection集合中的元素,必须领队于Collection对象。Iterator对象也被称为迭代器。定义了三个方法:

  • boolean hasNext():如果被迭代的集合元素还没有被遍历完,则返回true
  • Object next():返回集合里的下一个元素
  • void remove():删除集合里上一次next方法返回的元素
  1. import java.util.Iterator;
  2. import java.util.Collection;
  3. //创建一个集合
  4. Collection books = new HashSet();
  5. books.add("King");
  6. books.add("Moon");
  7. //获取books集合对应的迭代器
  8. Iterator it = books.iterator();
  9. while(it.hasNext()){
  10. //it.next()方法返回的数据类型是Object类型,需要强制类型转换
  11. String b = (String)it.next();
  12. if(book.equals("King"){
  13. //从集合中删除上一代next方法返回的元素
  14. it.remove();
  15. }
  16. }

使用foreach循环遍历集合元素

  1. import java.util.Collection;
  2. Collection books = new HashSet();
  3. books.add("King");
  4. books.add("Moon");
  5. for (Object obj : books){
  6. ……
  7. }

Set集合

Set集合与Collection集合基本上完全一样,它没有提供任何额外的方法,只是行为略有不同(Set不允许包含重复元素)。
Set根据equals方法判断两个对象是否相同,而不是==运算符。


HashSet类

HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能
HashSet具有以下特点:

  • 不能保证元素的排列顺序,顺序可能发生变化
  • HashSet不是同步的,如果多个线程同时访问一个HashSet,假设有两个或两个以上线程同时修改了HashSet集合时,则必须通过代码来保证其同步
  • 集合元素值可以是null

当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到hashCode值,然后根据该值决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet将会把它们存储在不同的位置,依然可以添加成功。

Hash集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等。


LinkedHashSet类

HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护存储元素的次序,这样使元素看起来是以插入的顺序保存的。即遍历此集合元素时,将会按元素的添加顺序来访问集合里的元素。
用法与HashSet类似,输出集合元素时,元素的顺序问题与添加的顺序一致。依然不允许集合元素重复。


TreeSet类

TreeSet是SortedSet的实现类,可以确保集合元素处于排序状态。与HashSet集合相比,TreeSet还提供了如下几个额外的方法

  • Comparator comparator():如果TreeSet采用了定制排序,则该方法返回定制排序所使用的Comparator;如果采用自然排序则返回null
  • Object first():返回集合里第一个元素
  • Object last():返回集合里的最后一个元素
  • Object lower(Object e):返回即使中位于指定元素之前的元素(即大于指定元素的最小元素,参考元素不需要是TreeSet集合里的元素)
  • Object higher(Object e): 与上一条类似但相反
  • SortedSet subSet(fromElement,toElement):返回此Set的集合范围从fromElement(包含)到toElement(不包含)
  • SortedSet headSet(toElement):返回此Set的子集,由小于toElement的元素组成
  • SortedSet tailSet(fromElement):返回此Set的子集,由大于或等于fromElement元素组成

TreeSet采用红黑树的数据结构来存储集合元素。支持两种排序方法:自然排序和定制排序,默认采用前者。


自然排序

TreeSet会调用集合元素的compareTo(Object obj)方法来比较元素之间的大小关系,然后将集合元素按升序排列,这种方式就是自然排序。
Java的一些常用类已经实现了Comparable接口(即提供了compareTo()方法),并提供了比较大小的标准。

  • BigDecimal、BigInteger及所有数值型对应的包装类:按数值大小比较
  • Character:按字符的UNICODE值进行比较
  • Boolean: true > false
  • String:按字符串中字符的UNICODE值进行比较
  • Date、Time:后面的时间、日期更大

如果试图把一个对象添加到TreeSet时,该对象的类必须实现Comparable接口,否则程序抛出异常。
大部分类实现compareTo(Object obj)方法时,都需要将obj强制类型转换成相同类型。当试图把一个对象添加到TreeSet集合时,TreeSet集合会调用该对象的compareTo(Object obj)方法与集合中的其他元素进行比较,这就要求集合中的其他元素与该元素是同一个类的实例。

对于TreeSet集合而言,它判断两个对象是否相等的唯一标准是:两个对象通过compareTo(Object obj)方法比较是否返回0。


定制排序

实现定制排序可以通过Comparator接口的帮助,该接口里包含一个int compare(T o1, T o2)方法,用于比较o1和o2的大小。
如需定制排序,则需要在创建TreeSet集合对象时,提供一个Comparator对象与该TreeSet集合关联

  1. import java.util.TreeSet;
  2. TreeSet ts = new TreeSet(new Comparator(){
  3. public int compare(Object o1, Object o2){
  4. ……
  5. }
  6. })

EnumSet类

EnumSet是专为枚举类设计的集合类,所有元素都必须是指定枚举类型的枚举值。EnumSet集合元素也是有序的,以枚举值在Enum类内的定义顺序来决定集合元素的顺序。
EnumSet在内部以位微量的形式存储,非常紧凑、高效,因此占用内存很小,而且运行效率很好。
EnumSet集合不允许加入null元素,否则会抛出异常
EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的static方法来创建EnumSet对象。详见API

Set的三个实现类HashSet、TreeSet和EnumSet都是线程不安全的。


List集合

List集合元素有序、可重复。集合中每个元素都有其对应的顺序索引,可用普通的for循环遍历集合元素。默认按元素添加顺序设置索引。

List作为Collection接口的子接口,当然可以使用Collection接口里的全部方法。而且由于List是有序集合,因此List集合里拉架了一些根据索引来操作集合元素的方法。

  • void add(int index, Object element):将element插入到List集合的index处
  • boolean addAll(int index, Collection c):将c所包含的所有元素都插入到List集合的index处
  • Object get(int index):返回index索引处的元素
  • int indexOf(Object o):返回o在List集合中第一次出现的位置索引
  • int lastIndexOf(Objectd o):与上相反
  • Object remove(int index):删除并返回index索引处的元素
  • Object set(int index, Object element):将index索引处的元素替换成element对象,返回新元素
  • List subList(int fromIndex ,int toIndex),返回从索引fromIndex(包含)到索引toIndex(不包含)处所有集合元素组成的子集合。

与Set集合相比,List增加了根据索引来插入、替换和删除集合元素的方法。
List判断两个对象相等只要通过equals()方法比较返回true即可。
List额外提供了一个listIterator()方法,返回一个ListIterator对象。ListIterator接口在Iterator接口基础上增加了如下方法:

  • boolean hasPrevious():返回该迭代器关联的集合是否还有上一个元素
  • Object previous():返回该迭代器的上一个元素
  • void add():在指定位置插入一个元素

须先采用正向迭代,才可以开始使用反向迭代。


ArrayList和Vector实现类

ArrayList和Vector作为List类的两个典型实现,完全支持前面介绍的List接口的全部功能。都是基于数组实现的List类,所以都封装了一个动态的、允许再分配的Object[]数组。ArrayList或Vector对象使用initialCapacity参数来设置该数组的长度,当向ArrayList或Vector中添加元素走出了该数组的长度时,它们的initialCapacity会自动增加。
当向ArrayList或Vector集合中添加大量元素时,可使用ensureCapacity(int minCapacity)方法一次性地增加initialCapacity。这样可以减少重分配的次数,从而提高性能。

  • void ensureCapacity(int minCapacity):如上
  • void trimToSize():调整ArrayList或Vector集合的Object[]数组长度为当前元素的个数,程序可用此方法来减少集合对象占有用的存储空间。

ArrayList和Vector用法几乎完全相同,Vector是一个古老的集合(从JDK 1.0 就有了)。尽量少用后者。
ArrayList是线程不安全的。仍然不推荐使用线程安全的Vector


Arrays.asList(Object[] obj)-固定长度的List

该方法对于基本数据类型的数组支持并不好,当数组是基本数据类型时不建议使用。
当使用asList()方法时,数组就和列表链接在一起了,当更新其中之一时,另一个将自动获得更新。注意:仅仅针对对象数组类型,基本数据类型数组不具备该特性

  1. int[] a = { 1, 2, 3, 4 };
  2. List aList = Arrays.asList(a);
  3. System.out.println(aList);

预期输出应该是1,2,3,4,但实际上输出的仅仅是一个引用@hash值, 这里它把a当成了一个元素。
对象类型的数组使用asList,是我们预期的

  1. Integer[] aInteger = new Integer[] { 1, 2, 3, 4 };
  2. List aList = Arrays.asList(aInteger);

Queue集合

Queue用于横批队列这种数据结构,队列通常是指“先进先出”(FIFO)的窗口。队列的头部保存在队列中存放时间最长的元素,队列的尾部保存在队列中存放时间最短的元素。新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素。通常,队列不允许随机访问队列中的元素。
Queue接口中定义了如下几个方法。

  • void add(Object e):将指定元素加入此队列的尾部
  • Object element():获取队列头部的元素,但是不删除该元素。
  • boolean offer(Object e):将指定元素加入此队列的尾部。当使用有容量限制的队列时,此方法通常比add(Object e)方法更好。
  • Object peek():获取队列头部的元素,但是不删除该元素。如果此队列为空,则返回null。
  • Object poll():获取队列头部的元素,并删除该元素。如果此队列为空则返回null
  • Object remove():获取队列头部的元素,并删除该元素。

Queue接口有一个PriorityQueue实现类。除此之外,Queue还有一个Deque接口(双端队列,全名double-ended-queue)。双端队列可以同时从两端来添加、删除元素,因此Deque的实现类既可以当队列使用,也可以当成栈使用。Java为Deque提供了ArrayDeque和LinkedList两个实现类。


PriorityQueue实现类

PriorityQueue保存队列元素按大小进行重新排序。因此当调用peek()方法或者polll()方法取出队列中的元素时,取出队列中最小的元素。

  1. import java.util.PriorityQueue;
  2. PriorityQueue pq = new PriorityQueue();
  3. //加入加个元素
  4. pq.offer(6);
  5. pq.offer(-3);
  6. pq.offer(9);
  7. pq.offer(0);
  8. //输出pq队列
  9. System.out.println(pq); //[-3, 0, 9, 6],这是调用toString方法,返回值并没有很好地按大小顺序排序。
  10. //访问队列的第一个元素,即最小值
  11. System.out.println(pq.poll()); //-3

PriorityQueue的元素也有自然排序和定制排序,对元素的要求与TreeSet一致。


Deque接口与ArrayDeque实现类

Deque接口是Queue接口的子接口,它代表一个双端队列,Deque接口里定义了一些双端队列的方法,这些方法允许从两端来操作队列的元素。

  • void addFirst(Object e)
  • void addLast(Object e): 以上两个方法分别将指定元素插入该双端队列的开头和末尾
  • Iterator descendingIterator():返回迭代器,以逆向顺序来迭代队列元素
  • Object getFirst()
  • Object getLast():以上两个方法分别获取但不删除该双端队列的第一个和最后一个元素
  • boolean offerFirst(Object e)
  • boolean offerLast(Object e):以上两方法分别将指定元素插入该双端队列的开头和末尾
  • Object peekFirst()
  • Object peekLast():以上两方法分别获取但不删除第一和最后一个元素
  • Object pollFirst()
  • Object pollLast():以上两方法分别获取并删除第一和最后一个元素
  • Object pop():出栈。pop出该双端队列所表示的栈的栈顶元素。相当于removeFirst()。
  • void push(Object e):入栈。将一个元素push进该双端队列所表示的栈的栈顶。相当于addFirst(e)。
  • Object removeFirst()
  • Object removeLast():以上两方法分别获取并删除第一和最后一个元素
  • Object removeFirstOccurrence(Object o)
  • Object removeLastOccurrence(Object o):以上两方法分别删除该双端队列第一和最后一次出现的元素o

Deque的方法与Queue的方法对照表

Queue的方法 Deque方法
add(e)/offer(e) addLast(e)/offerLast(e)
remove()/poll() removeFirst()/pollFirst()
element()/peek() getFirst()/peekFirst()

Deque的方法与Stack的方法对照表

Stack的方法 Deque方法
push(e) addLast(e)/offerLast(e)
pop() removeFirst()/pollFirst()
peek() getFirst()/peekFirst()

Deque接口提供了一个典型的实现类:ArrayDeque,它是一个基于数组实现的双端队列。

  1. import java.util.ArrayDeque;
  2. ArrayDeque stack = new ArrayDeque();
  3. //依次将三个元素push入栈
  4. stack.push("King");
  5. stack.push("Moon");
  6. stack.push("Almoye");
  7. //访问第一个元素但不将其pop出栈
  8. System.out.println(Stack.peek());
  9. ……

程序中需要使用“栈”这种数据结构时,推荐使用ArrayDeque或LinkedList,而不是Stack


LinkedList实现类

LinkedList实现了List和Deque接口,可以被当成双端队列来使用。
用法与ArrayDeque差不多,但是LinkedList的实现机制完全不同。ArrayList、ArrayDeque内部以数组的形式来保存集合中的元素,因此随机访问集合元素时有较好性能;而LinkedList内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能非常出色(只需改变指针所指的地址即可)。
对于所有内部基于数组的集合实现,例如ArrayList、ArrayDeque等,使用随机访问的性能比使用Iterator迭代访问的性能要好,因为随机访问会被映射成对数组元素的访问。


各种线性表的性能分析

Java提供的List就是一个线性表接口,而ArrayList、LinkedList又是线性表的两种典型实现:基于数组的线性表和基于链的线性表。Queue代表了队列,Deque代表了双端队列。

各种集合的性能对比

集合 实现机制 随机访问排名 迭代操作排名 插入操作排名 删除操作排名
数组 连续内存区保存元素 1 不支持 不支持 不支持
ArrayList/ArrayDeque 以数组保存元素 2 2 2 3
Vector 以数组区保存元素 3 3 3 3
LinkedList 以链表保存元素 4 1 1 1

Map

Map用于保存具有映射关系的数据。Map的key不允许重复,即同一个Map对象的任何两个key通过equals方法比较总返回false。
Set接口下有HashSet、LinkedHashSet、SortedSet(接口)、TreeSet、EnumSet等子接口和实现类,而Map接口下则有HashMap、LinkedHashMap、SortedMap(接口)、TreeMap、EnumMap等子接口和实现类。Map的这些实现类和子接口中key集的存储形式和对应Set集合中元素的存储形式完全相同。
从Java源码来看,Java是先实现了Map,然后通过包装一个所有value都为null的Map就实现了Set集合。

Map接口中定义了如下常用的方法:

  • void clear():删除该Map对象中的所有key-value对。
  • boolean containsKey(Object key):查询Map中是否包含指定的key,如果包含则返回true
  • boolean containsValue(Object value):如上
  • Set entrySet():返回Map中包含的key-value对所组成的Set集合,每个集合元素都是Map.Entry对象
  • Object get(Object key):返回指定key所对应的value;如果此Map中不包含该key,则返回null
  • boolean isEmpty():查询该Map是否为空
  • Set keySet():返回所该Map中所有key组成的Set集合
  • Object put(Object key, Object value):添加一个key-value对,如果当前Map中已有一个与该key相等的key-value对,则新的key-value对会覆盖原有的。
  • void putAll(Map m):将指定Map中的key-value对复制到本Map中
  • Object remove(Object key):删除指定key所对应的key-value对,返回被删除key所关联的value,如果该key不存在,则返回null
  • int size():返回该Map里的key-value对的个数
  • Collection values():返回该Map里所有value组成的Collection

Map中包括一个内部类Entry,该类封装了一个key-value对。Entry包含如下三个方法:

  • Object getKey():返回该Entry里包含的key值
  • Object getValue():返回该Entry里包含的value值
  • Object setValue(V value):设置该Entry里包含的value值,并返回新设置的value值。

HashMap和Hashtable实现类

HashMap和Hashtable的关系完全类似于ArrayList和Vector。
类似于HashSet,HashMap、Hashtable判断两个key相等的标准也是:两个key通过equals()方法比较返回true,两个key的hashCode值也相等。
另外,HashMap、Hashtable中还包含一个containsValue()方法,用于判断是否包含指定的value。判断标准是两个对象通过equals()方法返回true。


LinkedHashMap实现类

HashSet有一个子类是LinkedHashSet,HashMap也有一个LinkedHashMap子类,它也用双向链表来维护key-value对的次序(其实只需要考虑key的次序),该链表负责维护Map的迭代顺序,与key-value对的插入顺序保持一致。
LinkedHashMap可以避免对HashMap、Hashtable里的key-value对进行排序(只要插入key-value对时保持顺序即可),同时又可避免使用TreeMap所增加的成本。
LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能;但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。


使用Properties读写属性文件

Properties是Hashtable的子类,它可以把Map对象和属性文件关联起来,从而可以把Map对象中的key-value对写入属性文件中,也可以把属性文件中的“属性名=属性值”加载到Map对象中。由于属性文件里的属性名、属性值只能是字符串类型,所以Properties里的key、value都是字符串类型。
Properties类提供了如下三个方法来修改Properties里的key、value值。

  • String getProperty(String key):获取properties中指定属性名相对应的属性值,类似于Map的get(Object key)方法。
  • String getProperty(String key, String defaultValue):与上基本相似,若Properties中不存在指定的key时,返回指定默认值
  • Object setProperty(String key, String value):设置属性值,类似于Hashtable的put()方法。

除此之外,它还提供了两个读写Field文件的方法。

  • void load(InputStream inStream):从属性文件(以输入流表示)中加载key-value对,把加载到的key-value对追加到Properties里(Properties是Hashtable的子类,它不保证key-value对之间的次序)。
  • vodi store(OutputStream out, String comments):将Properties中的key-value对输出到指定的属性文件(以输出流表示)中。
  1. import java.util.Properties;
  2. Properties props = new Propertis();
  3. Properties props2 = new Properties();
  4. //向Properties中添加属性
  5. props.setProperty("username", "yeeku");
  6. //将Properties中的key-value对保存到a.ini文件中
  7. props.store(new FileOutputStream("a.ini"), "comment line");
  8. //将a.ini文件中的key-value对追加到props2中
  9. props2.load(new FileInputStream("a.ini"));

Properties还可以把key-value对以XML文件的形式保存起来,也可以从XML文件中加载key-value对,用法与此类似。


SortedMap接口和TreeMap实现类

正如Set接口派生出SortedSet子接口,SortedSet接口有一个TreeSet实现类一样,Map接口也派生出一个SortedMap子接口,它也有一个TreeMap实现类。
TreeMap是一个红黑树数据结构,每个key-value对即作为红黑树的一个节点。TreeMap存储key-value时,需要根据key对节点进行排序。TreeMap可以保证所有的key-value对处于有序状态。TreeMap也有自然和定制排序。
与TreeSet类似,TreeMap也提供了一系列根据key顺序访问key-value对的方法。

  • Map.Entry firstEntry():返回Map中最小key所对应的key-value对,若Map为空则返回null
  • Map.Entry lastEntry():同上
  • Map.Entry higherEntry(Object key):返回该Map中位于key后一位的key-value对(即大于指定key的最小key所对应的key-value对)。若Map为空则返回null
  • Map.Entry lowerEntry(Object key):同上。
  • Object firstKey():返回该Map中key的最小值,若Map为空则返回null
  • Object lastKey():同上。
  • Object higherKey(Object key):返回该Map中位于key后一位的key值(即大于指定key的最小key值)。若不存在则返回null
  • Object lowerKey(Object key):同上
  • NavigalbeMap subMap(Object fromKey, boolean fromInclusive, Object toKey, boolean toInclusive):返回该Map的子Map。看参数知其义
  • SortedMap subMap(Object fromKey, Object toKey):返回该Map的子Map,范围从fromKey(包括)到toKey(不包括)。
  • SortedMap tailMap(Object fromKey):返回该Map的子Map,其key范围是大于fromKey(包括)的所有key
  • SortedMap headMap(Object toKey):返回子Map,其key的范围是小于toKey(不包括)的所有key
  • NavigableMap tailMap(Object fromKey, boolean inclusive):返回子Map,见参数知其义
  • NavigableMap headMap(Object toKey, boolean inclusive):同上

WeakHashMap实现类

WeakHashMap与HashMap用法基本相似,区别在于HashMap的key保留了对实际对象的强引用,但WeakHashMap的key只保留了对实际对象的弱引用。


IdentifyHashMap实现类

这个Map实现类的实现机制与HashMap基本相似,但它在处理两个key相等时比较独特:在IdentityHashMap中,当且仅当两个key严格相等(key1 == key2)时,IdentityHashMap才认为两个key相等;对于普通的HashMap而言,只要key1和key2通过equals()方法比较返回true,且它们的hashCode值相等即可


EnumMap实现类

EnumMap所有key必须是单个枚举类的枚举值。创建EnumMap时必须显式或隐式指定它对应的枚举类。
EnumMap在内部以数组形式保存。
EnumMap根据key的自然顺序(即枚举值在枚举类中的定义顺序)来维护key-value对的顺序。
EnumMap不允许null作为key,但允许使用null作为value。

  1. import java.util.EnumMap;
  2. enum Season{
  3. SPRING, SUMMER, FALL, WINTER
  4. }
  5. ……
  6. EnumMap enumMap = new EnumMap(Season.class);

各Map实现类的性能分析

TreeMap通常比HashMap、Hashtable慢(尤其是插入、删除key-value对时),因为TreeMap底层采用红黑树来管理key-value对。好处是TreeMap中的key-value对总是处于有序状态,无须专门进行排序操作。当TreeMap被填充后,就可以调用keySet(),取得由key组成的Set,然后使用toArray()方法生成key的数组,接下来使用Arrays的binarySearch()方法在已排序的数组中快速地查询对象。
一般应用场景应该多考虑使用HashMap,它正是为快速查询设计的(HashMap底层其实也是采用数组来存储key-value对)。但如果程序需要一个总是排好序的Map时,则可以考虑使用TreeMap。
LinkedHashMap比HashMap慢一点,因为它需要维护链表来保持Map中key-value时的添加顺序。IdentityHashMap在性能上没有特别出色之处。EnumMap性能最好。


操作集合的工具类

排序操作

  • static void reverse(List list):反转
  • static void shuffle(List list):随机排序(洗牌)
  • static void sort(List list):根据元素的自然顺序升序排列
  • static void sort(List list, Comparator c):根据指定Comparator产生的顺序排序
  • static void swap(List list, int i ,int j):将i和j处元素进行交换
  • static rotate(List list, int distance):当distance为正数时,将list集合的后distance个元素“整体”移到前面;为负时相反。
  1. import java.util.*;
  2. ArrayList nums = new ArrayList();
  3. nums.add(2);
  4. nums.add(3);
  5. //反转List
  6. Collections.reverse(nums);
  7. //自然排序排序
  8. Collections.sort(nums);

查找、替换操作

  • static int binarySearch(List list, Object key):二分法搜索List,返回key的索引。List必须已经处于有序状态
  • static Object max(Collection c):最大值
  • static Object max(Collection c, Comparator comp):根据Comparator排序的最大值
  • static Object min(Collection c):同上
  • static Object min(collectin c, Comparator comp):同上
  • static void fill(List list, Object obj):用obj填充list
  • static int frequency(Collection c, Object obj):返回obj出现次数
  • static int indexOfSubList(List source, List target):返回子List对象在父List对象中第一次出现的位置索引,若无则返回-1.
  • static int lastIndexOfSubList(List source, List target):同上
  • static boolean replaceAll(List list, Object oldVal, Object newVal),用newVal替换list的所有oldVal

同步控制

Collections类提供了多个synchronizedXxx()方法,可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。
Java中常用的集合框架中的实现类HashSet、TreeSet、ArrayList、ArrayDeque、LinkedList、HashMap和TreeMap都是线程不安全的。

  1. import java.util.Collections;
  2. Collection c = Collections.synchronizedCollection(new ArrayList());
  3. List list = Collections.synchronizedList(new ArrayList());
  4. Set s = Collections.synchronizedSet(new HashSet());

设置不可变集合

Collections提供了如下三类方法来返回一个不可变的集合,List、Set或Map。

  • emptyXxx():返回一个空的、不可变的集合对象
  • singletonXxx()返回一个只包含指定对象(只有一个若一项元素)的、不可变的集合对象
  • unmodifiableXxx():返回指定集合对象的不可变视图。

泛型

在没有泛型之前,一旦把一个对象“丢进”Java集合中,集合就会忘记对象的类型,把所有的对象当成Object类型处理。当程序从集合中取出对象后,就需要进行强制类型转换,这种强制类型转换不仅使代码臃肿,而且容易引起ClassCastException异常
增加了泛型支持后的集合,完全可以记住集合中元素的类型,并可以在编译时检查集合中元素的类型,如果试图向集合中添加不满足类型要求的对象,编译器就会提示错误。增加泛型后的集合,可以让代码更加简洁,程序更加健壮。
Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。


泛型入门

Java集合缺点:把一个对象“丢进”集合,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就成了Object类型(其运行时类型没变)。
集合如此设计,原因是设计集合的程序员不会知道我们用它来保存什么类型的对象,这样带来两个问题:

  • 集合对元素类型没有任何限制,所以对象都可以放进去
  • 取出对象后需要进行强制类型转换,增加编程复杂度且会引起异常

使用泛型

  1. List<String> strList = new ArrayList<String>();

此处的List是带一个类型参数的泛型接口。


Java 7 泛型的“菱形”语法

Java 7 以前,如果使用带泛型的接口、类定义变量,那么调用构造器创建对象时,构造器的后面也必须带泛型,这显得多余了。
Java 7 开始,Java知构造器后不需要带完整的泛型信息,只要给出一对尖括号即可,Java可以推断尖括号里应该是什么泛型信息。

  1. List<String> strList = new ArrayList<>;

由于<>像菱形,这种语法也被称为“菱形”语法。


深入泛型

所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。Java 5 必定了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。


定义泛型接口、类

下面是Java 5 必定后List接口、Iterator接口、Map的代码片段:

  1. //定义接口时指定了一个类型形参,该形参名为E
  2. public interface List<E>{
  3. //在该接口里,E可作为类型使用
  4. void add(E x);
  5. Iterator<E> iterator();
  6. ……
  7. }
  8. //定义接口时指定了一个类型形参,该形参名为E
  9. public interface Iterator<E>{
  10. //在该接口里E完全可以作为类型使用
  11. E next();
  12. boolean hasNext();
  13. ……
  14. }
  15. //定义该接口时指定了两个类型形参,其形参名为K、V
  16. public interface Map<K , V>{
  17. //在该接口里K、V完全可以作为类型使用
  18. Set<K> keySet();
  19. V put(K key, V value)
  20. ……
  21. }

虽然程序只定义了一个List接口,但实际使用时可以产生无数多个List接口,只要为E传入不同的类型实参,系统就会多出一个新的List接口。必须指出:List绝不会被替换成ListString,系统没有进行源代码复制,二进制代码中没有,磁盘中没有,内存中也没有。
包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数多个逻辑上的子类,但这种子类在物理上并不存在。
我们可以为任何类、接口增加泛型声明。

  1. //定义Apple类时使用了泛型声明
  2. public class Apple<T>{
  3. //使用T类型形参定义实例变量
  4. private T info;
  5. public Apple(){}
  6. //下面方法中使用T类型形参来定义构造器
  7. public Apple(T info){
  8. this.info = info;
  9. }
  10. public void setInfo(T info){
  11. this.info = info;
  12. }
  13. public T getInfo(){
  14. return this.info;
  15. }
  16. }

注意,当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不可增加泛型声明。


从泛型类派生子类

当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类,但需要指出的是,当使用这些接口、父类时不能带包含类型形参。例如下面的代码就是错误的:

  1. public class A extends Apple<T>{}

方法中的形参代表变量、常量、表达式等数据,统称数据形参。定义方法时可以声明数据形参,调用方法时必须为这些数据形参传入实际的数据;与此类似的是,定义类、接口、方法时可以声明类型形参,使用类、接口、方法时应为类型形参传入实际的类型。
如果想从Apple类派生一个子类,则可以改为如下代码:

  1. public class A extends Apple<String>

调用方法时必须为所有的数据形参传入参数值,与调用方法不同的是,使用类、接口时可以不为类型形参传入实际的类型参数(这是指紧接在类名后的形参),即下面的代码也是正确的

  1. public class A extends Apple

如果从Apple类派生子类,则在Apple类中所有使用T类型形参的地方都将被替换成String类型,即它的子类将会继承到String getInfo()和void setInfo(String info)两个方法,如果子类要重写父类的方法就必须注意这一点。如下:

  1. //继承上面的Apple类
  2. public class A1 extends Apple<String>{
  3. //正确重写了父类的方法、返回值,与父类Apple<String>的返回值完全相同
  4. public String getInfo(){
  5. return "子类" + super.getInfo();
  6. }
  7. }

如果使用Apple类时没有传入实际的类型参数,Java编译器可能发出警告:使用了未经检查或不安全的操作——这就是泛型检查的警告。


并不存在泛型类

前面提到可以把ArrayList类当成ArrayList的子类,事实上,ArrayList类也确实像一种特殊的ArrayList类,这个ArrayList对象只能添加String对象作为集合元素。但实际上,系统并没有为ArrayList生成新的class文件,而且也不会把ArrayList当成新类来处理。
不管为泛型的类型开通传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只战胜一块内存空间,因此在静态方法、静态初始化块或静态变量的声明和初始化中不允许使用类型形参。
同样,系统并没有真正生成泛型类,instanceof运算符后不能使用泛型类。下面显示了如上错误:

  1. //下面代码错误,不能在静态变量声明中使用类型形参
  2. static T info;
  3. //下面代码错误,不能在静态方法声明中使用类型形参
  4. public static void bar(T msg){}
  5. //下面代码编译时引起错误:instanceof运算符后不能使用泛型类
  6. if ( cs instanceof List<String>){……}

类型通配符

上文的形参是指
正如前面讲的,使用一个泛型类时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参(继承父类泛型除外)。如果没有传入类型实参,编译器就会提出泛型警告。假设现在需要定义一个方法,该方法里有一个集合形参,集合形参的元素类型是不确定的,那应该怎么定义?
注意:如果Foo是Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G并不是G的子类型!这点非常值得注意,因为它与我们的习惯看法不同。
数组和泛型有所不同,假设Foo是Bar的一个子类型(子类或子接口),那么Foo[]依然是Bar[]的子类型;但G不是G的子类型。
如下代码是错误的:

  1. List<Integer> iList = new ArrayList<Integer>();
  2. //下面代码导致编译错误
  3. List<Number> nList = iList;

使用类型通配符

为了表示各种泛型List的父类,我们需要使用类型通配符,类型通配符是一个问号,将一个问题作为类型实参传给List集合,写作:List<?>(意思是未知类型元素的List)。这个问题被称为通配符,它的元素类型可以匹配任何类型。

  1. public void test(List<?> c){
  2. for (int i = 0 ; i < c.size(); i++){
  3. System.out.println(c.get(i));
  4. }
  5. }

但这种带通配符的List使表示它是各种泛型List的父类,并不能把元素加入到其中。例如,如下代码将会引起编译错误。

  1. List<?> c = new ArrayList<String>();
  2. //下面程序引起编译错误
  3. c.add(new Object());

因为我们不知道上面程序中c集合里元素的类型,所以不能向其中添加对象。根据前面的List接口定义的代码可以发现:add方法有类型参数E作为集合的元素类型,所以传给add的参数必须是E类的对象或者其子类的对象。但因为在该例中不知道E是什么类型,所以程序无法将任何对象“丢进”该集合。唯一的例外是null,它是所有引用类型的实例。
另一方面,程序可以调用get()方法来返回List集合指定索引处的元素,其返回值是一个未知类型,但可以肯定的是它总是一个Object。因此,把get()的返回值赋值给一个Object类型的变量,或者放在任何希望是Object类型的地方都可以。


设定通配符的上限

当直接使用List这种形式时,即表明这个List集合可以是任何泛型List的父类。但还有一种特殊的情形,我们不想使这个List是所有泛型List的父类,只想表示它是某一类泛型List的父类。

  1. //它表示所有Shape泛型List(即List后尖括号里的类型是Shape的子类)的父类
  2. List<? extends Shape>

List是受限制通配符的例子,此处的问号代表一个未知的类型,这个未知类型一定是Shape的子类型(也可以是Shape)本身,因此我们把Shape称为这个通配符的上限(upper bound)。
因为我们不知道这个受限制的通配符的具体类型,所以不能把Shape对象或其子类的对象加入这个泛型集合中,下面的代码就是错误的

  1. public void addRectangle(List<? extends Shaper> shapes){
  2. //下面代码引起编译错误
  3. shapers.add(0, new Rectangle());
  4. }

与普通通配符类似的是,shapes.add()的第二个参数类型是? extends Shape,它表示Shape未知的子类,我们无法准确知道这个类型是什么,所以无法将任何对象添加到这种集合中。


设定类型形参的上限

Java泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型开通时设定上限,用于表示传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。

  1. public class Apple<T extends Number>{
  2. T col;
  3. public static void main(String[] args){
  4. Apple<Integer> ai = new Apple<>();
  5. Apple<double> ad = new Apple<>();
  6. //下面代码将引发编译异常,String不是Number的子类型
  7. Apple<String> as = new Apple<>();
  8. }
  9. }

上面程序定义了一个Apple泛型类,该Apple类的类型形参的上限是Number类,这表明使用Apple类时为T形参传入的实际类型参数只能是Number或Number类的子类。
在一种更极端的情况下,程序需要为类型形参设定多个上限(至多有一个父类上限,可有有多个接口上限),表明该类型形参必须是其父类的子类(是父类本身也行),并且实现多个上限接口:

  1. public class Apple<T extends Number & java.io.Serializable>{
  2. ……
  3. }

泛型方法

前面介绍了在定义类、接口时可以使用类型形参,在该类的方法定义和变量定义、接口的方法定义中,这些类型形参可以被当成普通类型来用。在另外一些情况下,我们定义类、接口时没有使用类型形参,但定义方法时想自己定义类型形参,这也是可以的。Java 5 还提供了对泛型方法的支持。


定义泛型方法

所谓泛型方法(Generic Method),就是在声明方法时定义一个或多个类型的形参,格式如下:

  1. 修饰符 <T , S> 返回值类型 方法名(形参列表){…
  2. }

泛型的方法签名比普通方法的方法签名多了类型形参声明,放在方法修饰符和方法返回值类型之间。例:

  1. public class GenericMethodTest{
  2. static <T> void fromArrayToCollection(T[] a, Collection<T> c){
  3. for (T o : a){
  4. c.add(o);
  5. }
  6. }
  7. }
  8. public static void main(String[] args){
  9. Object[] oa = new Object[100];
  10. Collection<Object> co = new ArrayList<>();
  11. fromArrayToCollection(oa, co);//T代表Object类型
  12. String[] sa = new String[100];
  13. Collection<String> cs = new ArrayList<>();
  14. fromArrayToCollection(sa, cs);//T代表String类型
  15. fromArrayToCollection(sa, co);//T代表Object类型
  16. Integer[] ia = new Integer[100];
  17. Float[] fa = new Float[100];
  18. Number[] na = new Number[100];
  19. Collection<Number> cn = new ArrayList<>();
  20. fromArrayToCollection(ia, cn);//T代表Number类型
  21. fromArrayToCollection(fa, cn);//T代表Number类型
  22. fromArrayToCollection(na, cn);//T代表Number类型
  23. fromArrayTocollection(na, co);//T代表Object类型
  24. //下面代码出现编译错误,T代表String类型,但na是一个Number数组
  25. fromArrayToCollection(na, cs);
  26. }

与接口、类声明中定义的类型形参不同的是,方法声明中定义的形参只能在该方法里使用。
与类、接口中使用泛型参数不同的是,方法中的泛型参数无须显式传入实际类型参数,如上面程序所示,当程序调用fromArrayToCollection()方法时,无须在调用该方法前传入String、Object等类型,但系统依然可以知道类型形参的数据类型,因为编译器根据实参推断类型形参的值,它通常推断出最直接的类型参数。


泛型方法和类型通配符的区别

大多数时候可以用泛型方法来代替类型通配符。如:

  1. public interface Collection<E>{
  2. boolean containsAll(collection<?> c);
  3. boolean addAll(Collection<? extends E> c);
  4. ……
  5. }

上面两个方法的形参都采用了类型通配符,也可以采用泛型方法

  1. public interface Collection<E>{
  2. boolean <T> containsAll(collection<T> c);
  3. boolean <T extends E> addAll(Collection<T> c);
  4. ……
  5. }

上面方法使用了泛型形式,这时定义类型形参时设定上限。
上面两个方法中类型形参T只使用了一次,类型形参T产生的唯一效果是可以在不同的调用点传入不同的实际类型。对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。
泛型方法允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。
如果某个方法中一个形参a的类型或返回值的类型依赖于另一个形参b的类型,则形参b的类型声明不应该使用通配符——因为形参a或返回值的类型依赖于形参b的类型,如果形参b的类型无法确定,程序就无法定义形参a的类型。在这种情况下,只能考虑使用在方法签名中声明类型形参——也就是泛型方法。

如果有需要,我们可以同时使用泛型方法和通配符,如Java的Collections.copy()方法

  1. public class Collections{
  2. public static <T> void copy(List<T> dest, List<? extends T> src){……}
  3. }

把它改成 使用泛型方法,不使用类型通配符:

  1. class Collection{
  2. public static <T, S extends T> void copy(List<T> dest, List<S> src){……}
  3. }

上面类型形参S仅用了一次,没有其他参数的类型、方法返回值的类型依赖于它,那类型形参S就没有存在的必要,即可以用通配符来代替S。使用通配符比使用泛型方法(在方法签名中显式声明类型形参)更加清晰和准确。
类型通配符与泛型方法还有一个显著的区别:类型通配符即可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的类型形参必须在对应方法中显式声明(即不能单独用来定义一个变量)。


Java 7 的“菱形”语法与泛型构造器

正如泛型方法允许在方法签名中声明类型形参,Java也允许在构造器签名中声明类型形参,这样就产生了所谓的泛型构造器。
一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让Java根据数据参数的类型来“推断”类型形参的类型,而且程序员也可以显式地为构造器中的类型形参指定实际的类型。

  1. class Foo{
  2. public <T> Foo(T t){
  3. System.out.println(t);
  4. }
  5. }
  6. public class GenericConstructor{
  7. public static void main(String[] args){
  8. //泛型构造器中的T参数为String
  9. new Foo("疯狂Java讲义");
  10. //泛型构造器中的T参数为Integer
  11. new Foo(200);
  12. //显式指定泛型构造器中的T参数为String
  13. new <String> Foo("疯狂Android讲义");
  14. }
  15. }

如果程序显式指定了泛型构造器中声明的类型形参的实际类型,则不可以使用“菱形”语法。

  1. class MyClass<E>{
  2. public <T> MyClass(T t){
  3. System.out.println("t参数的值为:" + t);
  4. }
  5. }
  6. public class GenericDiamondTest{
  7. public static void main(String[] args){
  8. //MyClass类声明中的E形参是String类型,泛型构造器中声明的T形参是Integer类型
  9. MyClass<String> mc1 = new MyClass<>(5);
  10. //显式指定泛型构造器中声明的T形参是Integer类型
  11. MyClass<String> mc2 = new <Integer> MyClass<String>(5);
  12. //MyClass类声明中的E形参是String类型如果显式指定泛型构造器中声明的T形参是Integer类型,此时就不能使用菱形语法,下面代码是错的
  13. MyClass<String> mc3 = new <Integer> MyClass<>(5);
  14. }
  15. }

设定通配符下限

这个通配符表示它必须是Type本身,或是Type的父类。

  1. public static <T> T copy(Collection<? super T> dest, Collection<T> src){
  2. T last = null
  3. for (T ele : src){
  4. dest.add(ele);
  5. }
  6. }

泛型方法与重载

泛型允许设定通配符的上、下限,从而允许在一个类里包含如下两个方法定义:

  1. public class MyUtils{
  2. public static <T> void copy(Collection<T> dest, Collection<? extends T> src){...}
  3. public static <T> T copy(Collection<? super T> dest, Collection<T> src){...}
  4. }

两个方法的参数中,前一个集合里的元素类型都是后一集合里集合元素类型的父类。这个类仅包含这两个方法不会有任何错误,但只要调用这个方法就会引起编译错误。编译器无法确定想调用哪个copy方法。


擦除和转换

严格的泛型代码里,带泛型声明的类总应该带着类型参数,但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型参数,此时该类型参数被称作raw type (原始类型),默认是声明该参数时指定的第一个上限类型。
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有尖括号之间的类型信息都将被扔掉。即擦除。
从逻辑上看,List是List的子类,如果直接把后者对象赋给前者的对象应该引起编译错误,实际上不会,编译器仅仅提示“未经检查的转换”


泛型与数组

Java 5 的泛型有一个很重要的设计原则——如果一段代码在编译时没有提出“[unchecked]未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常。正是基于这个原因,数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符。但可以声明元素类型包含类型变量或类型开通的数组。
就是就说,只能声明List[]形式的数组,但是不能创建ArrayList[10]这样的数组对象。
与此类似的是,创建元素类型是类型变量的数组对象也将导致编译错误:

  1. <T> T[] makeArray(Collection<T> coll){
  2. //下面代码将导致编译错误
  3. return new T[coll.size()];
  4. }

因为类型变量在运行时并不存在,所以编译器无法确定实际类型是什么。


异常处理

Java的异常机制主要依赖于try、catch、finally、throw和throws五个关键字。
其中try关键字后紧跟一个花括号括起来的代码块(花括号不可省略),简称try块。它里面旋转可能引发异常的代码。
catch后对应异常类型和一个代码块,用于表明该catch块用于处理这种类型的代码块。
多个catch块后还可以跟一个finally块,finally块用于回收在try块里打开的物理资源,异常机制会保证finally块总被执行。
throws关键字主要在方法签名中使用,用于声明该方法可能抛出的异常
throw用于抛出一个实际的异常,throw可以单独作为语句使用,抛出一个具体的异常对象

Java将异常分为两种,Checked异常和Runtime异常,Java认为Checked异常都是可以在编译阶段被处理的异常,所以它强制程序处理所有的Checked异常;而Runtime异常则无须处理。


异常处理机制

Java的异常处理机制可以让程序具有极好的容错性。当程序出现意外情形时,系统会自动生成一个Exception对象来通知程序,从而实现将“业务功能实现代码”和“错误处理代码”分离,提供更好的可读性。


使用try...catch捕获异常

Java提出一种假设:如果程序可以顺利完成,那就“一切正常”,把系统的业务实现代码放在try块中定义,所有的异常处理逻辑放在catch块中进行处理。语法结构如下:

  1. try{
  2. //业务实现代码
  3. ...
  4. }
  5. catch (Exception e){
  6. alert 输入不合法
  7. goto retry
  8. }

如果执行try块里的业务逻辑代码时出现异常,系统自动生成一个异常对象,该异常对象被提交给Java运行时环境,这个过程被称为抛出(throw)异常。
当Java运行时环境收到异常对象时,会寻找能处理该异常对象的catch块,如果找到合适的catch块,则把该异常对象交给catch块处理,这个过程被称为捕获(catch)异常;如果Java运行时环境找不到捕获异常的catch块,则运行时环境终止,Java程序也将退出。

不管程序代码块是否处于try块中,甚至包括catch块中的代码,只要执行该代码块时出现了异常,系统总会自动生成一个异常对象。如果程序没有为这段代码定义任何的catch块,则Java运行时环境无法找到处理该异常的catch块,程序就在此退出。

try块里声明的变量是代码块内局部变量,它只在try块内有效,在catch块中不能访问该变量。


异常类的继承体系

Java把所有非正常情况分成两种:异常(Exception)和错误(Error),它们都继承Throwable父类。

Error错误,一般是指与虚拟机相关的问题,如系统崩溃、虚拟机错误、动态链接失败等,这种错误无法恢复或不可能捕获,将导致应用程序中断。通常应用程序无法处理这些错误,因此应用程序不应该试图使用catch块来捕获Error对象。在定义方法时,也无须在其throws子句中声明该方法可能抛出Error及其任何子类。

  1. public class DivTest{
  2. public static void main(String[] args){
  3. try{
  4. int a = Integer.parseInt(args[0]);
  5. int b = Integer.parseInt(args[1]);
  6. int c = a / b;
  7. System.out.println("您输入的两个数相除的结果是:" + c );
  8. }
  9. catch (IndexOutOfBoundsException ie){
  10. System.out.println("数组越界:运行程序时输入的参数个数不够");
  11. }
  12. catch (NumberFormatException ne){
  13. System.out.println("数字格式异常:程序只能接收整数参数");
  14. }
  15. catch (ArithmeticException ae){
  16. System.out.println("算术异常");
  17. }
  18. catch (Exception e){
  19. System.out.println("未知异常");
  20. }
  21. }
  22. }

Java运行时的异常处理逻辑可能有如下几种情形:

  • 如果运行该程序时输入的参数不够,将会发生数组越界异常,Java运行时将调用IndexOutOfBoundsException对应的catch块处理该异常
  • 如果运行该程序时输入的参数不是数字,而是字母,将发生数字格式异常,Java运行时将调用NumberFormatException异常对应的catch块处理该异常
  • 如果运行该程序时除数是0,将发生除0异常,Java运行时将调用ArithmeticException对应的catch块处理该异常\
  • 如果程序试图调用一个null对象的实例方法或实例变量时,就会引发NullPointerException异常。
  • 如果程序运行时出现其他异常,该异常对象总量Exception类或子类的实例,Java运行时将调用Exception对应的catch块处理该异常

运行异常捕获时不仅应该把Exception类对应的catch块放在最后,而且所有父类异常的catch块都应该排在子类异常catch块的后面(简称:先处理小异常,再处理大异常),否则将出现编译错误。


Java 7 提供的多异常捕获

在Java 7 以前,每个catch块只能捕获一种类型的异常;但从Java 7 开始,一个catch块可以捕获多种类型的异常

使用一个catch块捕获多种类型的异常时需要注意如下两个地方。

  • 捕获多种类型的异常时,多种异常类型之间应该用竖线(|)隔开。
  • 捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值
  1. public class MultiExceptionTest{
  2. public static void main(String[] args){
  3. try{
  4. int a = Integer.parseInt(args[0]);
  5. int b = Integer.parseInt(args[1]);
  6. int c = a / b;
  7. System.out.println("您输入的两个数相除的结果是:" + c );
  8. }
  9. catch (IndexOutOfBoundsException | NumberFormatException | ArithmeticException ie){
  10. SYstem.out.println("程序发生了数组越界、数字格式异常、算术异常之一");
  11. //捕获多异常时,异常变量默认有final修饰,所以下面代码有错
  12. ie = new ArithmeticException("test");
  13. }
  14. catch (Exception e){
  15. System.out.println("未知异常");
  16. //捕获一种类型的异常,异常变量没有final修饰,所以下面代码完全正确
  17. e = new RuntimeException("test");
  18. }
  19. }
  20. }

访问异常信息

如果程序需要在catch块中访问异常对象的相关信息,则可以通过访问catch块后的异常形参来获。
所有的异常形参都包含了如下几个常用方法。

  • getMessage():返回该异常的详细描述字符串
  • printStackTrace():将该异常的跟踪栈信息输出到标准错误输出
  • printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流
  • getStackTrace():返回该异常的跟踪栈信息
  1. try{
  2. FileInputStream fis = new FileInputStream("a.txt");
  3. }
  4. catch (IOException ioe){
  5. System.out.println(ioe.getMessage());
  6. ioe.printStackTrace();
  7. }

使用finally回收资源

有些时候,程序在try块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等),这些物理资源都必须显式回收。

  • Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所战用的内存

在哪里回收这些物理资源呢?假设程序在try块里进行资源回收,如果该try块的某条语句引起了异常,该语句后的其他语句通常不会获得执行的机会。如果在catch块里进行资源回收,但catch块完全有可能得不到执行。
为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。除非在try块、catch块中调用了退出虚拟机的方法。
完整的Java异常处理语法结构如下:

  1. try{
  2. //业务实现代码
  3. ...
  4. }
  5. catch (SubException e){
  6. //异常处理块1
  7. ...
  8. }
  9. catch (SubException2 e){
  10. //异常处理块2
  11. ...
  12. }
  13. ...
  14. finally{
  15. //资源回收块
  16. ...
  17. }

异常处理语法结构中只有try块是必需的,也就是说如果没有try块,则不能有后面的catch块和finally块;catch块和finally块都是可选的,但至少出现其中之一或都出现。

  1. public class FinallyTest{
  2. public static void main(String[] args){
  3. FileInputStream fis = null;
  4. try{
  5. fis = new FileInputStream("a.txt");
  6. }
  7. catch (IOException ioe){
  8. System.out.println(ioe.getMessage());
  9. //return语句强制方法返回
  10. return;
  11. //使用exit退出虚拟机
  12. //System.exit(1);
  13. }
  14. finally{
  15. //关闭磁盘文件,回收资源
  16. if (fis != null){
  17. try{
  18. fis.close();
  19. }
  20. catch (IOException ioe){
  21. ioe.printStackTrace();
  22. }
  23. }
  24. System.out.println("执行finally块里的资源回收!");
  25. }
  26. }
  27. }

通常情况下,不要在finally块中使用如return或throw等导致方法终止的语句,否则会导致try块、catch块中的return、throw语句失效。
当Java程序执行try块、catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找异常处理流程中是否包含finally块,如果没有finally块,程序立即执行return或throw语句,方法终止;如果有finally块,系统立即开始执行finally块——只有当finally块执行完成后,系统才会再次跳回来执行try块、catch块里的return或throw语句;如果finally块里也使用了return或throw等导致方法终止的语句,finally块已经终止了方法,系统将不会跳回去执行try块、catch块里的任何代码。


异常处理的嵌套

异常处理流程代码可以放在任何能放可执行性代码的地方,因此完整的异常处理代码既可放在try块里,也可放在catch块里,还可放在finally块里。
异常处理嵌套的深度没有很明确的限制,但通常没有必要使用超过两层的嵌套异常处理,层次太深没有必要,而且导致程序可读性降低。


Java 7 的自动关闭资源的try语句

Java 7 增强了try语句的功能——它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源是指那些必须在程序结束时显式关闭的资源(比如数据库连接、网络连接等),try语句在该语句结束时自动关闭这些资源。
为了保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseable或Closeable接口,实现这两个接口就必须实现colse()方法。
Closeable是AutoCloseable的子接口,可以被自动关闭的资源类要么实现AutoCloseable接口,要么实现Closeable接口。Closeable接口里的close()方法声明抛出了IOException,因此它的实现类在实现close()方法时只能声明抛出IOException或其子类;AutoCloseable接口里的close()方法声明抛出了Exception,因此它的实现在实现close()方法时可以声明抛出任何异常。

  1. public class AutoCloseTest{
  2. public static void main(String[] args) throws IOException{
  3. try (
  4. //声明、初始化两个可关闭的资源,try语句会自动关闭这两个资源
  5. BufferedReader br = new BufferedReader(new FileReader("AutoCloseTest.java"));
  6. PrintStream ps = new PrintStream(new FileOutputStream("a.txt")))
  7. {
  8. //使用两个资源
  9. System.out.println(br.readLine());
  10. ps.println("庄生晓梦迷蝴蝶");
  11. }
  12. }
  13. }

上面程序中粗体字代码分别声明、初始化了两个IO流,由于BufferedReader、PrintStream都实现了Closeable接口,而且它们放在try语句中声明、初始化,所以try语句会自动关闭它们。因此上面程序是安全的。
自动关闭资源的语句相当于包含了隐式的finally块(这个finally块用于关闭资源),因此这个try语句可以既没有catch块,也没有finally块。

Java 7 几乎把所有的“资源类”(包括文件IO的各种类、JDBC编程的Connection、Statement等接口……)进行了改写,改写后资源类都实现了AutoCloseable或Closeable接口。


Checked异常和Runtime异常体系

Java的异常分为两大类:Checked异常和Runtime异常(运行时异常)。所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。
只有Java语言提供了Checked异常,其他语言都没有。Java程序必须显式处理Checked异常,如果程序没有处理该异常,则无法通过编译。
对Checked异常的处理方式有如下两种:

  • 当前方法明确知道如何处理该异常,程序应该使用try...catch块来捕获该异常不,然后在对应的catch块中修复该异常。
  • 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常

Runtime异常则更灵活,Runtime异常无须显式声明抛出,如果程序需要捕获Runtime异常,也可以使用try...catch块来实现
大部分的方法总是不能明确地知道如何处理异常,因此只能声明抛出该异常,这种情况如此普遍,所以Checked异常降低了程序开发的生产率和代码的执行效率。


使用throws声明抛出异常

使用throws声明抛出异常的思路是:当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常将交给JVM处理。JVM对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行。
throws声明抛出只能在方法签名中使用,throws可以声明抛出多个异常类,多个异常类之间用逗号隔开。语法格式如下:

  1. throws ExceptionClass1, ExceptionClass2...

下面程序使用了throws来声明抛出IOException异常,一旦使用throws语句声明抛出该异常,程序就无须使用try...catch块来捕获该异常了

  1. public static void main(String[] args) throws IOException{
  2. FileInputStream fis = new FIleInputStream("a.txt");
  3. }

上面程序声明不处理IOException异常,将该异常交给JVM处理,所以程序一旦遇到该异常,JVM就会打印该异常的跟踪栈信息,并结束程序。

如果某段代码中调用了一个带throws声明的方法,该方法声明抛出了Checked异常,则表明该方法希望它的调用者来处理该异常。也就是说,调用该方法时要么放在try块中显式捕获该异常,要么放在另一个带throws声明抛出的方法中。

  1. public class ThrowsTest{
  2. public static void main(String[] args) throws Exception{
  3. //因为test()方法声明抛出IOException异常,所以调用该方法的代码要么处于try...catch块中,要么处于另一个带throws声明抛出的方法中
  4. test();
  5. }
  6. public static void test() throws IOException{
  7. //因为FileInputStream的构造器声明抛出IOException异常,所以调用 FileInputStream的代码要么处于try...catch块中,要么处于另一个带throws声明抛出的方法中
  8. FileInputStream fis = new FileInputStream("a.txt");
  9. }
  10. }

使用throws声明抛出异常时有一个限制,就是方法重写时“两小”中的一条规则:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
使用Checked异常至少有两大不便之处:

  • 对于程序中的Checked异常,Java要求必须显式捕获并处理该异常,或者显式声明抛出该异常,这样就增加了编程复杂度
  • 如果在方法中显式声明抛出Checked异常,将会方法签名与异常耦合,如果该方法是重写父类的方法,则该方法抛出的异常还会受到被重写方法所抛出异常的限制。

使用Runtime异常时,程序无须在方法中声明抛出Checked异常,一旦发生了自定义错误,程序只管抛出Runtime异常即可。如果程序需要在合适的地方捕获异常并对异常进行处理,则一样可以在try...catch块来捕获Runtime异常。


使用throw抛出异常

当程序出现错误时,系统会自动抛出异常;除此之外,Java也允许程序自行抛出异常,自行抛出异常使用throw语句来完成(注意此处throw后面没有s,与前面声明抛出的throws是有区别的)。


抛出异常

如果程序中的数据、执行与既定的业务不符,这就是一种异常,必须由程序员来决定抛出,系统无法抛出这种异常。
如果需要在程序中自行抛出异常,则应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一种异常实例,而且每次只能抛出一个异常实例。语法格式如下:

  1. throw ExceptionInstance;

不管系统自动抛出的异常,还是程序员手动抛出的异常,Java运行环境对异常的处理没有任何差别。
如果throw语句抛出的异常是Checked异常,则该throw 语句要么处于try块里,显式捕获该异常,要么放在一个带throws声明抛出的方法中,即把该异常交给方法的调用者处理;如果throw语句抛出的异常是Runtime异常,则该语句无须放在try块里,也无须放在带throw声明抛出的方法中;程序既可以显式使用try...catch来捕获并处理该异常,也可以完全不理会该异常,把该异常交给该方法调用者处理。

  1. public class ThrowTest{
  2. public static void main(String[] args){
  3. try{
  4. //调用声明抛出Checked异常的方法,要么显式捕获该异常,要么在main方法中再次声明抛出
  5. throwChecked(-3);
  6. }
  7. catch (Exception e){
  8. System.out.println(e.getMessage());
  9. }
  10. //调用声明抛出Runtime异常的方法既可以显式捕获该异常,也可不理会该异常
  11. throwRuntime(3);
  12. }
  13. public static void throwChecked(int a) throws Exception{
  14. if (a > 0){
  15. //自行抛出Exception异常,该代码必须处于try块里,或处于带throws声明的方法中
  16. throw new Exception("a的值大于0,不符合要求");
  17. }
  18. }
  19. public static void throwRuntime(int a){
  20. if (a > 0){
  21. //自行抛出RuntimeException异常,既可以显式捕获该异常,也可完全不理会该异常,把该异常交给该方法调用者处理
  22. throw new RuntimeException("a的值大于0,不符合要求");
  23. }
  24. }
  25. }

自定义异常类

在通常情况下,程序很少会自行抛出系统异常,因为异常的类名通常也包含了该异常的有用令牌,所以在选择抛出异常时,应该选择合适的异常类,从而可以明确地描述该异常情况。在这种情形下,应用程序常常需要抛出自定义异常。
用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个是无参数的构造器;另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(也就是异常对象的getMessage()方法的返回值)。

  1. public class AuctionException extends Exception{
  2. //无参数的构造器
  3. public AuctionException(){}
  4. //带一个字符串参数的构造器
  5. public AuctionException(String msg){
  6. super(msg);
  7. }
  8. }

上面程序super调用可以将此字符串参数传给异常对象的message属性,该message属性就是该异常对象的详细描述信息。
如果需要自定义Runtime异常,只需将上面程序中的Exception基类改为RuntimeException基类,其他地方无须修改。
大部分情况下,创建自定义异常都可以采用与上方相似的代码完成,只需改变AuctionException异常的类名即可,让该异常类的类名可以准确描述该异常。


catch和throw同时使用

前面介绍的异常处理方式有如下两种:

  • 在出现异常的方法内捕获并处理异常,该方法的调用者将不能再次捕获该异常
  • 该方法签名中声明抛出该异常,将该异常完全交给方法调用者处理。

在实际应用中往往需要更复杂的处理方式——当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获到异常。
为了实现这种通过多个方法协作处理同一个异常的情形,可以在catch块中结合throw语句来完成。

  1. public class AuctionTest{
  2. private double initPrice = 30.0;
  3. //因为该方法中显式抛出了AuctionException异常
  4. //所以此处需要声明抛出AuctionException异常
  5. //AuctionException即上文的代码
  6. public void bid(String bidPrice) throws AuctionException{
  7. double d = 0.0;
  8. try{
  9. d = Double.parseDouble(bidPrice);
  10. }
  11. catch (Exception a){
  12. //此处完成本方法中可以对异常执行的修复处理
  13. //此处仅仅是在控制台打印异常的跟踪栈信息
  14. e.printStackTraceI();
  15. //再次抛出自定义异常
  16. throw new AuctionException("竞拍价必须是数值,不能包含其他字符!");
  17. }
  18. if (initPrice > d) {
  19. throw new AuctionException("竞拍价比起拍价低,不允许竞拍!");
  20. }
  21. initPrice = d;
  22. }
  23. public static void main(String[] args){
  24. AuctionTest at = new AuctionTest();
  25. try{
  26. at.bid("df");
  27. }
  28. catch (AuctionException ae){
  29. //再次捕获到bid()方法中的异常,并对该异常进行处理
  30. //将该异常的详细描述信息输出到标准错误输出
  31. System.err.println(ae.getMessage());
  32. }
  33. }
  34. }

这种catch和throw结合使用的情况在大型企业级应用中非常常用。企业级应用对异常的处理通常分成两个部分:

  • 应用后台需要通过日志来记录异常发生的详细情况
  • 应用还需要根据异常向应用使用者传达某种提示。

在这种情形下,所有异常都需要两个方法共同完成,也就必须将catch和throw结合使用。


Java 7 增强的throw语句

对于如下代码

  1. try {
  2. new FileOutputStream("a.txt");
  3. }
  4. catch (Exception ex){
  5. ex.printStackTrace();
  6. throw ex;
  7. }

上面代码再次抛出了捕获到的异常,程序捕获该异常时,声明该异常的类型为Exception;但实际上try块中可能只调用了FileOutputStream构造器,这个构造器声明只是抛出了FileNotFoundException异常。
在Java 7 以前,由于在捕获该异常时声明ex的类型是Exception,因此Java编译器认为这段代码可能抛出Exception异常,所以包含这段代码的方法通常需要声明抛出Exception异常。
从Java 7 开始,Java编译器会检查throw语句抛出异常的实际类型,这样编译器知道上述代码实际上只可能抛出FileNotFoundException异常,因此在方法签名中只要声明抛出FileNotFoundException异常即可


异常链

对于真实的企业级应用而言,常常有严格的分层关系,上层功能的实现严格依赖于下层的API,也不会跨层访问。

表现层:用户界面----->中间层:实现业务逻辑----->持久层:保存数据

对于采用上图结构的应用,当业务逻辑层访问持久层出现SQLException异常时,程序不应该把底层的SQLException异常传到用户界面,有如下两个原因:、

  • 对于正常用户而言,他们不想看到底层SQLException异常,SQLException异常对他们使用该系统没有任何帮助
  • 对于恶意用户而言,将SQLException异常暴露出来不安全

把底层的原始异常直接传给用户是一种不负责任的表现。通常的做法是:程序先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息,这各处理方式被称为异常转译。
假设程序需要实现工资计算的方法,则程序应该采用如下结构的代码来实现该方法:

  1. public calSal() throws SalException{
  2. try{
  3. //实现结算工资的业务逻辑
  4. ...
  5. }
  6. catch (SQLException sqle){
  7. //把原始异常记录下来,留给管理员
  8. ...
  9. //下面异常中的message就是对用户的提示
  10. throw new SalException("访问底层数据库出现异常");
  11. }
  12. catch (Exception e){
  13. //把原始异常记录下来,留给管理员
  14. ...
  15. //下面异常中的message就是对用户的提示
  16. throw new SalException("系统出现异常");
  17. }
  18. }

这种把原始异常信息隐藏起来,仅向上提供必要的异常提示信息的处理方式,可以保证底层异常不会扩散到表现层,可以避免向上暴露太多的实现细节,这完全符合面向对象的封装原则。
这种把捕获一个异常然后接着抛出另一个异常,并把原始异常信息保存下来是一种典型的链式处理(23种设计模式之一:职责链模式),也被称为“异常链”。
在JDK 1.4 以前,程序员必须自己编写代码来保持原始异常信息。从JDK 1.4 以后,所有Throwable的子类在构造器中都可以接收一个cause对象作为参数。这个cause就用来表示原始异常,这样可以把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,你也能通过这个异常链追踪到异常最初发生的位置。如果我们希望上面的SalException可以追踪到最原始的异常信息,则可以改写成如下形式:

  1. public calSal() throws SalException{
  2. try{
  3. //实现结算工资的业务逻辑
  4. ...
  5. }
  6. catch (SQLException sqle){
  7. //把原始异常记录下来,留给管理员
  8. ...
  9. //下面异常中的sqle就是原始异常
  10. throw new SalException(sqle);
  11. }
  12. catch(Exception e){
  13. //把原始异常记录下来,留给管理员
  14. ...
  15. //下面异常中的e就是原始异常
  16. throw new SalException(e);
  17. }
  18. }

上面代码创建SalException对象时,传入了一个Exception对象,而不是传入一个String对象,这就需要SalException类有相应的构造器。从JDK 1.4 以后,Throwable基类已经有了一个可以接收Exception方法,所以可以采用如下代码来定义SalException类。

  1. public class SalException extends Exception{
  2. public SalException(){}
  3. public SalException(String msg){
  4. super(msg);
  5. }
  6. public SalException(Throwable t){
  7. super(t);
  8. }
  9. }

Java的异常跟踪栈

异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出结果,我们可以找到异常的源头,并跟踪到异常一路触发的过程。
在面向对象的编程中,大多数复杂的操作都会被分解成一系列方法的调用。所以面向对象的应用程序运行时,经常会发生一系列方法调用,从而形成“方法调用栈”,异常的传播则相反:只要异常没有被完全捕获(包括异常没有被捕获,或异常被处理后重新抛出了新异常),异常从发生异常的方法逐渐向外传播,首先传给该方法的调用者,该方法调用者再次传给其调用者……直到最后传到main方法,如果main方法依然没有处理该异常,JVM会中止该程序,并打印异常的跟踪栈信息。
跟踪栈总是最内部的被调用方法逐渐上传,直到最外部业务操作的起点,通常就是程序的入口main方法或Thread类的run方法(多线程的情形)。
虽然printStackTrace()方法可以很方便地用于追踪异常的发生情况,可以用它来高度程序,但在最后发布的程序中,应该避免使用它;而应该对捕获的异常进行适当的处理,而不是简单地将异常的跟踪栈信息打印出来。


异常处理规则

成功的异常处理应该实现如下4个目标:

  • 使程序代码混乱最小化
  • 捕获并保留诊断信息
  • 通知合适的人员
  • 适用合适的方式结束异常活动

不要过度使用异常

滥用异常机制会带来一些负面影响。过度使用异常主要有两个方面:

  • 把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地抛出异常来代替所有的错误处理
  • 使用异常处理来代替流程控制

熟悉了异常使用方法后,程序员可能不再愿意编写烦琐的错误处理代码,而是简单地抛出异常。实际上这样做是不对的,对于完全已知的错误,应该编写处理这种错误的代码,增加程序的健壮性;对于普通的错误,应该编写处理这种错误的代码,增加程序的健壮性。只有对外部的、不能确定和预知的运行时错误才使用异常。
异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分享,因此绝不要使用异常处理来代替正常的业务逻辑判断。
另外,异常机制的效率比正常的流程控制效率差,所以不要使用异常处理来代替正常的流程控制。异常只应该用于处理非正常的情况,对于一些完全可预知,而且处理方式清楚的错误,程序应该提供相应的错误处理代码,而不是将其笼统地称为异常。


不要使用过于宏大的try块

try块里的代码过于庞大,业务过于复杂,就会造成try块中出现异常的可能性大大增加,从而导致分析异常原因的难度也大大增加。而且当try块过于庞大时,就在try块后紧跟大量的catch块才可以针对不同的异常提供不同的处理逻辑。同一个try块后紧跟大量的catch则需要分析它们之间的逻辑关系,反而增加了编程复杂度。
正确的做法是,把大块的try块分割成多个可能出现异常的程序段落,并把它们放在单独的try块中,从而分别捕获并处理异常。


避免使用Catch All语句

所谓Catch All语句指的是一种异常捕获模块,它可以处理程序所有可能异常。

  1. try{
  2. //可能引发Checked异常的代码
  3. }
  4. catch (Throwable t){
  5. //进行异常处理
  6. t.printStackTrace();
  7. }

编写关键程序时应该避免使用这种异常处理方式,它有两点不足之处:

  • 所有的异常都采用相同的处理方式,这将导致无法对不同的异常分情况处理,如果要分情况处理,则需要在catch块中使用分支语句进行控制,这是得不偿失的做法。
  • 这种捕获方式可能将程序中的错误、Runtime异常等可能导致程序终止的情况全部捕获到,从而“压制”了异常。如果出现了一些“关键”异常,那么此异常也会被“静悄悄”地忽略。

Catch All语句不过是一种通过避免错误处理而加快编程进度的机制,应尽量避免在实际应用中使用这种语句


不要忽略捕获到的异常

不要忽略异常!既然已捕获到异常,那catch块理应做些有用的事情——处理并修复这个错误。catch块整个为空,或者仅仅打印出错信息都是不妥的!
通常建议对异常采取适当措施,比如:

  • 处理异常。对异常进行合适的修复,然后绕过异常发生的地方继续执行;或用别的数据进行计算,以代替期望的方法返回值;或者提示用户重新操作……总之,对于Checked异常,程序应该尽量修复
  • 重新抛出新异常。把当前运行环境下能做的事情尽量做完,然后进行异常转译,把异常包装成当前层的异常,重新抛出给上层调用者。
  • 在合适的层处理异常。如果当前层不清楚如何处理异常,就不要在当前层使用catch语句来捕获该异常,直接使用throws声明抛出该异常,让上层调用者来负责处理该异常。

AWT编程

Java使用AWT和Swing类完成图形用户界面编程,其中AWT的全称是抽象窗口工具集(Abstract Window Toolkit),它是Sun最早提供的GUI库,这个GUI库提供了一些基本功能,但这个GUI库的功能比较有限,所以后来又提供了Swing库。通过使用AWT和Swing提供的图形界面组件库,Java的图形用户界面编程非常简单,程序只要依次创建所需的图形组件,并以合适的方式将这些组件组织在一起,就可以开发出非常美观的用户界面。
程序以一种“搭积木”的方式将这些图形用户组件组织在一起,就是实际可用的图形用户界面,但这些图形用户界面还不能与用户交互,为了实现图形用户界面与用户交互操作,还应为程序提供事件处理,事件处理负责让程序可以响应用户的动作。


GUI(图形用户界面)和AWT

GUI即图形用户界面(Graphics User Interface)。当JDK 1.0 发布时,Sun提供了AWT,它从不同平台的窗口系统中抽取出共同组件,当程序运行时,将这些组件的创建和动作委托给程序所在的运行平台。简而言之,当使用AWT编写图形界面程序时,程序仅指定了界面组件的位置和行为,并未提供真正的实现,JVM调用操作系统本地的图形界面来创建和平台一致的对等体。
使用AWT创建的图形界面应用和所在的运行平台有相同的界面风格。Sun希望采用这种方式来实现“Write Once, Run Anywhere”的目标。
但在实际应用中,AWT出现了如下几个问题:、

  • 使用AWT做出的图形用户界面在所有的平台上都显得很丑陋,功能也非常有限。
  • AWT为了迎合所有主流操作系统的界面设计,AWT组件只能使用这些操作系统上图形界面组件的次,所以不能使用特定操作系统上复杂的图形界面组件,最多只能使用4种字体。、
  • AWT用的是非常笨拙的、非面向对象的编程模式

1996年,Netscape公司开发了一套工作方式完全不同的GUI库,简称为IFC(Internet Foundation Classes),这套GUI库的所有图形界面组件都是绘制在空白窗口上的,只有窗口本身需要借助于操作系统的窗口实现。IFC真正实现了各种平台上的界面一致性。不久,Sun和Netscape合作完善了这种方法,并创建了一套新的用户界面库:Swing。AWT、Swing、辅助功能API、2D API以及播放API共同组成了JFC(Java Foundation Classes,Java基础类库),其中Swing组件全面替代了Java 1.0中的AWT组件,但保留了Java 1.1中的AWT事件模型。总体上,AWT是图形用户界面编程的基础,Swing组件替代了绝大部分AWT组件,对AWT图形用户界面编程有极好的补充和加强。
Swing并没有完全替代AWT,而是建立在AWT基础之上,Swing仅提供了能力更强大的用户界面组件,即使是完全采用Swing编写的GUI程序,也依然使用AWT的事件处理机制。
所有和AWT编程相关的类都放在java.awt包以及它的子包中,AWT编程中有两个基类:Component(普通组件)和MenuComponent(菜单相关组件)。其中Component代表一个能以图形化方式显示出来,并可与用户交互的对象,使用Button代表一个按钮,TextField代表一个文本框等;而MenuComponent则代表图形界面的菜单组件,包括MenuBar(菜单条)、MenuItem(菜单项)等子类。
除此之外,AWT图形用户界面编程里还有两个重要的概念:Container和LayoutManager,其中Container是一种特殊的Component,它代表一种容器,可以盛装普通的Component;而LayoutManager则是容器管理其他组件而已的方式


AWT容器

任何窗口都可被分解成一个空的容器,容器里盛装了大量的基本组件,通过设置这些基本组件的大小、位置等属性,就可以将该空的容器和基本组件组成一个整体的窗口。
容器(Container)是Component的子类,因此容器对象本身也是一个组件,具有组件的所有性质,可以调用Component类的所有方法。
Componenet类提供了如下几个常用方法来设置组件的大小、位置和可见性等:

  • setLocation(int x,int y):设置组件的位置。
  • setSize(int width, int height):设置组件的大小。
  • setBounds(int x, int y,int width, int height):同时设置组件的位置、大小
  • setVisible(Boolean b):设置该组件的可见性

容器还可以盛装其他组件,容器类(Container)提供了如下几个常用方法来访问容器里的组件。

  • Component add(Component comp):向容器中添加其他组件(该组件既可以是普通组件,也可以是容器),并返回被添加的组件
  • Component getComponentAt(int x, int y):返回指定点的组件
  • int getComponentCount():返回该容器内组件的数量
  • Component[] getComponents():返回该容器内的所有组件

AWT主要提供了如下两种主要的容器类型:

  • Window:可独立存在的顶级窗口
  • Panel:可作为容器容纳其他组件,但不能独立存在,必须被添加到其他容器中(如Window、Panel或者Applet等)

摘自网络:
Pane(窗格)指的是一个独立窗口中的窗格,比如.CHM帮助文档中左边一个索引窗格,右边一个正文窗格;再比如Eclipse左边一个包资源管理器窗格,中间一个编辑器窗格等等。
Panel(面板)指的是一个面板,用它来对一些控件进行分组,就像组合框控件,即Visual Studio里面用的Group Box Control;而在一些软件界面里面也可以表现为工具条,比如编辑工具条、文件工具条、绘图工具条等等(其实这些工具条在开发实现上是一些窗口,即主窗口中的子窗口,学习过WIN32 API编程的朋友会更好理解)。

Frame是常见的窗口,它是Window类的子类,具有如下几个特点:

  • Frame对象有标题,允许通过拖拉来改变窗口的位置、大小。
  • 初始化时为不可见,可用setVisible(true)使其显示出来
  • 默认使用BorderLayout作为其布局管理器

下面通过Frame创建了一个窗口。

  1. import java.awt.Frame;
  2. public class FrameTest{
  3. public static void main(String[] args){
  4. Frame f = new Frame("测试窗口");
  5. f.setBounds(30, 30, 250, 200);//设置窗口的大小、位置
  6. f.setVisible(true);//将窗口显示出来(Frame对象默认处于隐藏状态)
  7. }
  8. }

运行上面程序出现的窗口,如果单击右上角的X,该窗口不会关闭,这是因为我们还未为该窗口编写任何事件响应。

Panel(面板)是AWT中另一个典型的容器,它代表不能独立存在、必须放在其他容器中的容器。Panel外在表现为一个矩形区域,该区域内可盛装其他组件。Panel容器存在的意义在于为其他组件提供空间,Panel容器具有如下几个特点:

  • 可作为容器来盛装其他组件,为放置组件提供空间
  • 不能单独存在,必须放置到其他容器中
  • 默认使用FlowLayout作为其布局管理器

下面使用Panel作为容器来盛装一个文本框和一个按钮,并将该Panel对象添加到Frame对象中:

  1. import java.awt.*;
  2. public class PanelTest{
  3. public static void main(String[] args){
  4. Frame f = new Frame("测试窗口");
  5. Panel p = new Panel();
  6. p.add(new TextField(20));
  7. p.add(new Button("单击我"));
  8. f.add(p);//将Panel容器添加到Frame窗口中
  9. f.setBounds(30, 30, 250, 120);
  10. f.setVisible(true);
  11. }
  12. }

ScrollPane是一个带滚动条的窗口,它也不能单独存在,必须被添加到其他容器中。ScrollPane有如下几个特点:

  • 可作为容器来盛装其他组件,当组件占用空间过大时,ScrollPane自动产生滚动条。当然也可以通过指定特定的构造器参数来指定默认具有滚动条
  • 不能单独存在,必须放置到其他容器中
  • 默认使用BorderLayout作为其布局管理器。ScrollPane通常用于盛装其他容器,所以通常不允许改变ScrollPane的布局管理器

下面使用ScrollPane容器来代替Panel容器

  1. import java.awt.*;
  2. public class ScrollPaneTest{
  3. public static void main(String[] args){
  4. Frame f = new Frame("测试窗口");
  5. ScrollPane sp = new ScrollPane(ScrollPane.SCROLLBARS_ALWAYS);//创建ScrollPane并指定总是具有滚动条
  6. sp.add(new TextField(20));
  7. sp.add(new Button("单击我"));
  8. f.add(sp);//将ScrollPane容器添加到Frame对象中
  9. f.setBounds(30, 30, 250, 120);
  10. f.setVisible(true);
  11. }
  12. }

上面程序虽然向ScrollPane容器添加了一个文本框和一个按钮,但只能看到一个按钮,却看不到文本框。这是因为ScrollPane使用BorderLayout布局管理器的缘故,而BorderLayout导致了该容器中只有一个组件被显示出来。


AWT容器的继承关系图

Container是Componenet的子类。
Window、Panel和ScrollPane是Container的子类
Frame和Dialog是Window的子类。Applet是Panel的子类


布局管理器

为了使生成的图形用户界面具有良好的平台无关性,Java语言提供了布局管理器这个工具来管理组件在容器中的布局而不使用直接设置组件位置和大小的方式。
组件在不同的平台上大小可能不一样,Java提供了LayoutManager,它根据运行平台来调整组件的大小,程序员只须为容器选择合适的布局管理器。
所有的AWT容器都有默认的布局管理器,如果没有为容器指定布局管理器,则该容器使用默认的布局管理器。为容器指定布局管理器通过调用容器对象的setLayout(LayoutManager lm)方法来完成。如下代码所示:

  1. c.setLayout(new XxxLayout());

AWT提供了FlowLayout、BorderLayout、GridLayout、GridBagLayout、CardLayout5个常用的布局管理器。Swing还提供了一个BoxLayout布局管理器。


FlowLayout布局管理器

在FlowLayout布局管理器中,组件像水流骊样向某个方法流动(排列),遇到障碍(边界)就折回,从头开始排列。FlowLayout布局管理器默认从左向右排列所有组件,遇到边界就会折回下一行重新开始。
FlowLayout有如下3个构造器:

  • FlowLayout():使用默认的对齐方式及默认的垂直间距、水平间距创建FlowLayout布局管理器
  • FlowLayout(int align):使用指定的对齐方式及默认的垂直间距、水平间距创建FlowLayout布局管理器
  • FlowLayout(int align, int hgap, int vgap):使用指定的对齐方式及指定的垂直间距、水平间距创建FlowLayout布局管理器

上面hgap、vgap代表水平间距、垂直间距。align表明FlowLayout中组件的排列方向(从左向右、从右向左、从中间向两边等),该参数应该使用FlowLayout类的静态常量:FlowLayout.LEFT、FlowLayout.CENTER、FlowLayout.RIGHT。

Panel和Applet默认使用FlowLayout布局管理器。

下面程序将一个Frame改变使用FlowLayout布局管理器(默认用BorderLayout)。

  1. import java.awt.Frame;
  2. Frame f = new Frame("测试窗口");
  3. //设置Frame容器使用FlowLayout布局管理器
  4. f.setLayout(new FlowLayout(FlowLayout.LEFT, 20, 5));
  5. //向容器添加10个按钮
  6. for (int i = 0; i < 10; i++){
  7. f.add(new Button("按钮" + i));
  8. }
  9. //设置容器为最佳大小
  10. f.pack();
  11. f.setVisible(true);

pack()方法 是Window容器提供的一个方法,该方法用于将容器调整到最佳大小。通过Java编写图形用户界面程序时,很少直接设置窗口的大小,通常用pack()方法调整。


BorderLayout布局管理器

BorderLayout将容器分为EAST、SOUTH、WEST、NORTH、CENTER五个区域。北和南占据上下两边。中间由西、中、东瓜分。
当改变使用BorderLayout的容器大小时,NORTH、SOUTH和CENTER区域水平调整,而EAST、WEST和CENTER区域垂直调整。使用BorderLayout有如下两个注意点:

  • 当向使用BorderLayout布局管理器的容器中添加组件时,需要指定要添加到哪个区域中,如果未指定,则默认添加到中间。
  • 如果向同一个区域添加多个组件时,后放入的组件会覆盖先放入的组件

Frame、Dialog、ScrollPane默认使用BorderLayout布局管理器

BorderLayout有如下两个构造器:

  • BorderLayout():使用默认的水平间距、垂直间距创建BorderLayout布局管理器
  • BorderLayout(int hgap,int vgap):使用指定的水平间距、垂直间距创建创建BorderLayout布局管理器。

BorderLayout用如下几个静态常量来指定添加到哪个区域中:EAST、NORTH、WEST、SOUTH、CENTER。

  1. Frame f = new Frame("测试窗口");
  2. //设置Frame容器使用BorderLayout布局管理器
  3. f.setLayout(new BorderLayout(30, 5));
  4. f.add(new Button("南"), SOUTH);
  5. ...
  6. //默认添加到中间区域中
  7. f.add(new Button("中"));
  8. ...
  9. //设置窗口为最佳大小
  10. f.pack();
  11. f.setVisible(true);

BorderLayout最多只能放置5个组件,但可以放置少于5个组件。如果某区域没有放置组件,该区域不会出现空白,旁边区域的组件会自动占据该区域。
虽然BorderLayout最多只能放置5个组件,但因为容器也是一个组件,所以我们可以先向Panel里添加多个组件,再把Panel添加到BorderLayout布局管理器中,从而让BorderLayout布局管理中的实际组件数远远超出5个。

  1. Frame f = new Frame("测试窗口");
  2. f.setLayout(new BorderLayout(30,5));
  3. f.add(new Button("南"), BorderLayout.SOUTH);
  4. f.add(new Button("北"), BorderLayout.NORTH);
  5. //创建一个Panel对象
  6. Panel p = new Panel();
  7. //向Panel对象中添加两个组件
  8. p.add(new TextField(20));
  9. p.add(new Button("单击我"));
  10. //默认添加到中间区域中,向中间区域添加一个Panel容器
  11. f.add(p);
  12. f.add(new Button("东"), BorderLayout.EAST);
  13. f.pack();
  14. f.setVisible(true);

GridLayout布局管理器

GridLayout将容器分割成纵横线分隔的网格,每个风格所占的区域大小相同。当向使用GridLayout布局管理器的容器中添加组件时,默认从左向右、从上向下依次添加到每个风格中。
与FlowLayout不同的是,放置在GridLayout中的各组件的大小由组件所处的区域来决定(每个组件将自动占满整个区域)
GridLayout有如下两个构造器:

  • GridLayout(int rows, int cols):采用指定的行数、列数,以及默认的横向间距、纵向间距将容器分割成多个网格
  • GridLayout(int rows, int cols, int hgap, int vgap):采用指定的行数、列数,以及指定的横向间距、纵向间距将容器分割成多个网格。

如下程序结合BorderLayout和GridLayout开发了一个计算器的可视化窗口

  1. Frame f = new Frame("计算器");
  2. Panel p1 = new Panel();
  3. p1.add(new TextField(30));
  4. f.add(p1, BorderLayout.NORTH);
  5. Panel p2 = new Panel();
  6. //设置Panel使用GridLayout布局管理器
  7. p2.setLayout(new GridLayout(3,5,4,4));
  8. String[] name = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "-", "*", "/", ".");
  9. //向Panel中依次添加15个按钮
  10. for (int i = 0; i < name.length; i++){
  11. p2.add(new Button(name[i]));
  12. }
  13. //默认将Panel对象添加到Frame窗口的中间
  14. f.add(p2);
  15. f.pack();
  16. f.setVisible(true);

GridBagLayout布局管理器

GridBagLayout功能最强大也最复杂,与GridLayout不同的是在GridBagLayout中,一个组件可以跨越一个或多个网格,并可以设置各风格的大小互不相同,从而增加了布局的灵活性。当窗口大小发生变化时,GridBagLayout也可以准确地控制窗口各部分的拉伸
为了处理GridBagLayout中GUI组件的大小、跨越性,Java提供了GridBagConstraints对象,该对象与特定的GUI组件关联,用于控制该GUI组件的大小、跨越性。

使用GridBagLayout的步骤如下:
<1>创建GridBagLayout,并指定GUI容器使用该布局管理器

  1. GridBagLayout gb = new GridBagLayout();
  2. container.setLayout(gb);

<2>创建GridBagConstraints对象,并设置该对象相关属性(用于设置受该对象控制的GUI组件的大小、跨越性等)。

  1. gbc.gridx = 2;//设置受该对象控制的GUI组件位于网格的横向索引
  2. gbc.gridy = 1;//位于网格的纵向索引
  3. gbc.gridwidth = 2;//横向跨越2格
  4. gbc.gridheight = 1;//纵向跨越1格

<3>调用GridBagLayout对象的方法来建立GridBagConstraints对象和受控制组件之间的关联

  1. gb.setConstraints(c,gbc);//设置c组件受gbc对象控制

<4>添加组件,与采用普通管理器添加组件的方法完全一样

  1. container.add(c);

如果需要向窗口中添加多个GUI组件,则需要多次重复步骤2~4。由于GridBagConstraints对象可以多次重用,所以实际上只需要创建一个GridBagConstraints对象,每次添加GUI组件之前先改变GridBagConstraints对象的属性即可。

GridBagConstraints类具有如下几个属性:

  • gridx、gridy:设置GUI组件左上角所在网格的横、纵向索引(GridBagLayout左上角风格的索引为0、0)。这两个值还可以是GridBagConstraints.RELATIVE(这是默认值),它表明当前组件紧跟在上一个组件之后。
  • gridwidth、gridheight:设置GUI组件横、纵向跨越多少个网格,默认值都是1.如果设置这两个属性值为GridBagConstraints.REMAINDER,这表明受该对象控制的GUI组件是横向、纵向最后一个组件;如果设置这两个属性值为GridBagConstraints.RELATIVE,则表明受该对象控制的GUI组件是横向、纵向倒数第二个组件。
  • fill:设置GUI组件如何占据空白区域。取值如下:
    GridBagConstraints.NONE:GUI组件不扩大
    GridBagConstraints.HORIZONTAL:水平扩大以占据空白区域
    GridBagConstraints.VERTICAL:垂直扩大以占据空白区域
    GridBagConstraints.BOTH:水平、垂直同时扩大以占据空白区域
  • ipadx、ipady:设置GUI组件横向、纵向内部填充的大小,即在该组件最小尺寸的基础上还需要增大多少。如果设置了这两个属性,则组件横向大小为最小宽度再加ipadx*2像素,纵向大小为最小高度再加ipady*2像素。
  • insets:设置GUI组件的外部填充的大小,即该组件边界和显示区域边界之间的距离。
  • anchor:设置GUI组件在其显示区域中的定位方式。属性如下:
    GridBagConstraints.CENTER:中间
    GridBagConstraints.NORTH: 上中
    GridBagConstraints.NORTHWEST:左上
    GridBagConstraints.NORTHEAST:右上
    GridBagConstraints.SOUTH:下中
    GridBagConstraints.SOUTHWEST:左下
    GridBagConstraints.SOUTHEAST:右下
    GridBagConstraints.EAST:右中
    GridBagConstraints.WEST:左中
  • weightx、weighty:设置GUI组件占据多余空间的水平、垂直增加比例(也叫权重)。这两个属性的默认值是0,即该组件不占据多余空间。假设某个容器的水平线上包括3个GUI组件,它们的水平增加比例分别是1、2、3,但窗口宽度增加60像素时,则第一个组件宽度增加10像素,第二个组件宽度增加20像素,第三个组件宽度增加30。如果其增加比例为0,则表示不会增加

如果希望某个组件的大小随容器的增大而增大,则必须同时设置该组件的GridBagConstraints对象的fill属性和weightx、weighty属性。

  1. public class Test{
  2. private Frame f = new Frame("测试窗口");
  3. private GridBagLayout gb = new GridBagLayout();
  4. private GridBagConstraints gbc = new GridBagConstraints();
  5. private Button[] bs = new Button[10];
  6. //初始化
  7. public void init(){
  8. f.setLayout(gb);
  9. //创建十个按钮
  10. for(int i = 0; i < bs.length ; i++){
  11. bs[i] = new Button("按钮" + i);
  12. }
  13. //所有组件都可以在横向、纵向上扩大
  14. gbc.fill = GridBagConstraints.BOTH;
  15. gbc.weightx = 1;
  16. addButton(bs[0]);
  17. addButton(bs[1]);
  18. addButton(bs[2]);
  19. //横向最后一个组件
  20. gbc.gridwidth = GridBagConstraints.REMAINDER;
  21. addButton(bs[3]);
  22. //横向不会扩大
  23. gbc.weightx = 0;
  24. addButton(bs[4]);
  25. //横跨两个网格
  26. gbc.gridwidth = 2;
  27. addButton(bs[5]);
  28. //横跨1个网格
  29. gbc.gridwidth = 1;
  30. //纵跨2个网格
  31. gbc.gridheight = 2;
  32. //横向最后一个组件
  33. gbc.gridwidth = GridBagConstraints.REMAINDER;
  34. addButton(bs[6]);
  35. //横跨1格,纵跨2格
  36. gbc.gridwidth = 1;
  37. gbc.gridheight = 2;
  38. //纵向扩大权重为1
  39. gbc.weighty = 1;
  40. addButton(bs[7]);
  41. //纵向不会扩大
  42. gbc.weighty = 0;
  43. //横向最后一个组件
  44. gbc.gridwidth = GridBagConstraints.REMAINDER;
  45. //纵跨1格
  46. gbc.gridheight = 1;
  47. addButton(bs[8]);
  48. addButton(bs[9]);
  49. f.pack();
  50. f.setVisible(true);
  51. }
  52. private void addButton(Button button){
  53. gb.setConstraints(button, gbc);//button受gbc控制
  54. f.add(button);
  55. }
  56. public static void main(String[] args){
  57. new Test().init();
  58. }
  59. }

CardLayout布局管理器

CardLayout以时间而非空间来管理它里面的组件,它将加入容器的所有组件看成一叠卡片,每次只有最上面的那个Component才可见。
CardLayout提供了如下两个构造器

  • CardLayout():创建默认的CardLayout布局管理器
  • CardLayout(int hgap, int vgap):指定左右、上下边界的间距

CardLayout用于控制组件的5个常用方法:

  • first(Container target):显示target容器中的第一张卡片
  • last(Container target):显示最后一张卡片
  • previous(Container target):显示前一张卡片
  • next(Container target):显示后一张卡片
  • show(Container target, String name):显示指定名字的卡片
  1. import java.awt.*;
  2. import java.awt.event.*;
  3. public class Test{
  4. Frame f = new Frame("测试窗口");
  5. String[] names = {"第一张", "第二张", "第三张", "第四张", "第五张"};
  6. Panel pl = new Panel();
  7. public void init(){
  8. final CardLayout c = new CardLayout();
  9. pl.setLayout(c);
  10. for(int i = 0; i < names.length; i++){
  11. pl.add(names[i], new Button(names[i]));
  12. }
  13. Panel p = new Panel();
  14. //控制显示上一张的按钮
  15. Button previous = new Button("上一张");
  16. previous.addActionListener(new ActionListener(){
  17. public void actionPerformed(ActionEvent e){
  18. c.previous(pl);
  19. }
  20. });
  21. //控制显示下一张的按钮
  22. Button next = new Button("下一张");
  23. next.addActionListener(new ActionListener(){
  24. public void actionPerformed(ActionEvent e){
  25. c.next(pl);
  26. }
  27. });
  28. //控制显示第一张的按钮
  29. Button first = new Button("第一张");
  30. first.addActionListener(new ActionListener(){
  31. public void actionPerformed(ActionEvent e){
  32. c.first(pl);
  33. }
  34. });
  35. //控制显示最后一张的按钮
  36. Button last = new Button("最后一张");
  37. last.addActionListener(new ActionListener(){
  38. public void actionPerformed(ActionEvent e){
  39. c.last(pl);
  40. }
  41. });
  42. //控制根据Card名显示的按钮
  43. Button third = new Button("第三张");
  44. third.addActionListener(new ActionListener(){
  45. public void actionPerformed(ActionEvent e){
  46. c.show(pl, "第三张");
  47. }
  48. });
  49. p.add(previous);
  50. p.add(next);
  51. p.add(first);
  52. p.add(last);
  53. p.add(third);
  54. f.add(pl);
  55. f.add(p, BorderLayout.SOUTH);
  56. f.pack();
  57. f.setVisible(true);
  58. }
  59. public static void main(String[] args){
  60. new Test().init();
  61. }
  62. }

绝对定位

Java容器中采用绝对定位的步骤如下

  1. 将Container的布局管理器设成null: setLayout(null)
  2. 向容器中添加组件时,先调用setBounds()或setSize()方法来设置组件的大小、位置。或者直接创建GUI组件时通过构造参数指定该组件的大小、位置,然后将该组件添加到容器中。
  1. Frame f = new Frame("测试窗口");
  2. Button b1 = new Button("第一个按钮");
  3. Button b2 = new Button("第二个按钮");
  4. f.setLayout(null);
  5. b1.setBounds(20, 30, 90, 28);
  6. f.add(b1);
  7. b2.setBounds(50, 45, 120, 35);
  8. f.add(b2);
  9. f.setBounds(50, 50, 200, 100);
  10. f.setVisible(true);

绝对定位更灵活,但使GUI界面失去跨平台特性。


BoxLayout布局管理器

GridBagLayout虽然功能强大但太复杂了,所以Swing引入了一个新的布局管理器BoxLayout,它保留了GridBagLayout的很多优点,但却没那么复杂。BoxLayout可以在垂直和水平两个方向上摆放GUI组件。
BoxLayout提供了如下一个简单的构造器

  • BoxLayout(Container target, int axis):指定创建基于target容器的BoxLayout布局管理器,里面的组件安axis方向排列。其中Axis有BoxLayout.X_AXIS(横向)和BoxLayout.Y_AXIS(纵向)两个方向
  1. Frame f = new Frame("Test");
  2. f.setLayout(new BoxLayout(f, BoxLayout.Y_AXIS));
  3. f.add(new Button("Fist Button");
  4. ...

BoxLayout通常和Box窗口结合使用,Box是一个特殊的容器,它有点像Panel,但Box默认使用BoxLayout布局管理器,Panel默认用FlowLayout。
Box提供了如下两个静态方法来创建Box对象:

  • createHorizontalBox():创建一个水平排列组件的Box容器
  • createVerticalBox():创建垂直排列组件的Box

一旦获得了Box容器后,就可以使用Box来盛装普通的GUI组件,然后将这些Box组件添加到其他容器中。从而形成整体的窗口布局。

  1. Frame f = new Frame("Test");
  2. Box horizontal = Box.createHorizontalBox();
  3. Box vertical = Box.createVerticalBox();
  4. public void init(){
  5. horizontal.add(new Button("水平按钮一"));
  6. horizontal.add(new Button("水平按钮二"));
  7. vertical.add(new Button("垂直按钮一"));
  8. vertical.add(new Button("垂直按钮二"));
  9. f.add(horizontal, BorderLayout.NORTH);
  10. f.add(vertical);
  11. f.pack();
  12. f.setVisible(true);
  13. }

BoxLayout没有提供设置间距的构造器和方法,因为BoxLayout采用另一种方式来控制组件的间距——BoxLayout使用Glue(橡胶)、Strut(支架)、和RigidArea(刚性区域)的组件来控制组件间的距离。其中Glue代表可以在横向、纵向两个方向上同时拉伸的空白组件(间距),Strut代表可以在横向、纵向任意一个方向上拉伸的空白组件(间距),RigidArea代表不可拉伸的空白组件(间距)。
Box提供了如下5个静态方法来创建Glue、Strut和RigidArea

  • createHorizontalGlue()
  • createVerticalGlue()
  • createHorizontalStrut(int width)
  • createVerticalStrut(int height)
  • CreateRigidArea(Dimension d);

上面5个方法都返回Component对象(代表间距),程序可以将这些分隔Component添加到两个普通的GUI之间,用以控制组件的间距。
BoxLayout是Swing提供的布局管理器,所以用于管理Swing组件将会有更好的表现。


AWT常用组件

AWT组件需要调用运行平台的图形界面来创建和平台一致的对待体,因此AWT只能使用所有平台都支持的公共组件,所以AWT只提供了一些常用的GUI组件。


基本组件

  • Button:按钮,可接受单击操作
  • Canvas:用于绘图的画布
  • Checkbox:复选框组件(也可变成单选框组件)
  • CheckboxGroup:用于将多个CheckBox组件合成一组,一组Checkbox组件将只有一个可以被选中,即全部变成单选框组件
  • Choice:下拉式选择框组件
  • Frame:窗口
  • Label:标签,用于放置提示性文件
  • List:列表框,可以添加多项条目
  • Panel:不能单独存在的基本容器类,必须放到其他窗口中。
  • Scrollbar:滑动条组件。如果需要用户输入位于某个范围的值,就可以使用滑动条组件,比如调色板中设置RGB的3个值所用的滑动条。当创建一个滑动条时,必须指定它的方向、初始值、滑块的大小、最小值和最大值。
  • ScrollPane:带水平及垂直滚动条的容器组件
  • TextArea:多行文本域
  • TextField:单行文本框

用法详见API。掌握它们的用法之后,就可以借助IDE工具来设计GUI界面,使用IDE工具可以更快地设计出更美观的GUI界面。


对话框Dialog

Dialog是Window类的子类,是一个容器类,属于特殊组件。对话框是可以独立存在的顶级窗口,但它通常依赖于其他窗口(parent窗口),对方框有模式和非模式两种。模式对话框被关闭之前,它依赖的窗口无法获得焦点。
Dialog的构造器可能有如下3个参数:

  • owner:指定该对话框所依赖的窗口,既可以是窗口,也可以是对话框。
  • title:标题
  • modal:是否模式
  1. Dialog d1 = new Dialog(f, "模式对话框", true);

Dialog类还有一个子类:FileDialog,它代表一个文件对话框。
构造器支持parent、title和mode三个构造参数。mode指定该窗口是用于打开或保存文件,对应参数值为:FileDialog.LOAD、FileDialog.SAVE
FileDialog的modal取决于运行平台的实现。
FileDialog提供了如下两个方法来获取被打开/保存文件的绝对路径

  • getDirectory():绝对路径
  • getFile():文件名

事件处理

AWT编程中,所有事件必须由特定对象(事件监听器)来处理,而Frame和组件本身并没有事件处理能力

Java事件模型的流程

  • Event Source:事件源。即各个组件
  • Event:事件。封装了GUI组件上发生的特定事件(用户操作)。
  • Event Listener:事件监听器。响应各种事件,调用对应的事件处理器(即事件监听器里的实例方法)

事件和事件监听器

事件源即组件,事件由系统自动产生。
事件监听器必须实现事件监听器接口。
AWT提供了大量的事件监听器接口用于实现不同类型的事件监听器,用于监听不同类型的事件。AWT中提供了丰富的事件类,用于封装不同组件上所发生的特定操作。AWT的事件类都是AWTEvent类的子类,AWTEvent是EventObject的子类。EventObject代表更广义的事件对象,包括Swing组件上所触发的事件、数据库连接所触发的事件等。
AWT的事件分两大类:低级和高级事件


低级事件

低级事件是指基于特定动作的事件,

  • ComponentEvent:组件事件。组件尺寸、位置、显隐状态
  • ContainerEvent:容器事件。容器里添加或删除组件。
  • WindowEvent:窗口事件。窗口状态发生变化。
  • FocusEvent:焦点事件。组件得失焦点
  • KeyEvent:键盘事件。按下、松开、单击
  • MouseEvent:鼠标事件
  • PaintEvent:组件绘制事件。调用update/paint方法来呈现自身时触发,并非专用于事件处理模型。

高级事件(语义事件)

高级事件是基于语义的事件。可以不和特定动作相关联,而依赖于触发此事件的类。

  • ActionEvent:动作事件。按钮、菜单被单击,TextField中按回车。
  • AdjustmentEvent:调节事件。滑动条上移动滑块以调节数值
  • ItemEvent:选项事件。选中或取消选中某项。
  • TextEvent:文本事件。文本发生改变。

事件、监听器接口和处理器之间的对应关系

一般直接把上面的XxxEvent改成XxxListener即为监听器接口,只有MouseEvent还有个额外的MouseMotionListener,在某个组件上移动鼠标。
实现监听器接口就可以实现对应的事件处理器,然后通过addXxxListener()方法将事件监听器注册给指定的组件(事件源)。当组件上发生特定事件时,对应事件监听器里的方法将被触发。


事件适配器

大部分时候程序无须监听窗口的每个动作。有时只须为用户单击窗口的X按钮提供响应即可——即windowClosing事件处理器。但因为该监听器实现了WindowListener接口,实现该接口就不得不实现该接口里的每个抽象方法。这是非常烦琐的事,为此,AWT提供了事件适配器。
事件适配器是监听器接口的空实现(实现了每个方法,但方法体为空)。当需要创建监听器时,可以通过继承事件适配器,仅重写自己感兴趣的方法。
把XxxListener改成XxxAdapter即可。只包含一个方法的监听器接口则没有对应的适配器。
有适配器的事件:Container Focus Component Key Mouse MouseMotion Window

  1. Frame f = new Frame("Test");
  2. //关闭窗口
  3. f.addWindowListener(new WindowAdapter(){
  4. public void windowClosing(WindowEvent e){
  5. System.exit(0);
  6. }
  7. });

使用内部类实现监听器

实现事件监听器对象有如下几种形式:

  • 内部类形式
  • 外部类形式
  • 类本身作为事件监听器类
  • 匿名内部内形式

示例见上文


使用外部类实现监听器

比较少见,主要有两原因:

  • 事件监听器通常属于特定的GUI,定义成外部类不利于提高程序的内聚性
  • 外部类形式的事件监听器不能自由访问创建GUI界面类中的组件,编程不够简洁

如果某事件监听器需要被多个GUI界面共享,而且主要完成某种业务逻辑的实现,则可以用外部类形式。
一般不推荐将业务逻辑实现写在事件监听器中,这会导致程序的显示逻辑和业务逻辑耦合,从而拉架程序后期的维护难度。可以考虑使用业务逻辑组件来定义业务逻辑功能,再让事件监听器来调用业务逻辑组件的方法。


类本身作为事件监听器类

GUI界面类直接作为监听器类,是早期AWT事件编程常用形式,有两个缺点。

  • 混乱的程序结构。GUI界面职责主要是完成界面初始化工作,但此时还需包含事件处理方法,降低了程序的可读性。
  • 如果GUI界面类需要继承事件适配器,将导致该GUI界面类不能继承其他父类。

匿名内部类实现监听器

大部分时候事件处理器没有复用价值。此方式应用最广泛。


AWT菜单

菜单条、菜单和期间项

AWT中的菜单由如下几个类组合而成

  • MenuBar:菜单条。菜单的容器。
  • Menu:菜单组件。菜单项的容器,它也是MenuItem的子类,可作为菜单项使用
  • PopupMenu:上下文菜单组件(右键菜单组件)
  • MenuItem:菜单项组件。
  • CheckboxMenuItem:复选框菜单项组件。
  • MenuShortcut:菜单快捷键组件

MenuBar可用于盛装Menu,Menu可用于盛装MenuItem(包括Menu)。PopupMenu无须使用MenuBar盛装。
Menu、MenuItem的构造器都可以接收一个字条串参数,作为标签文本。
MenuItem还可以接收一个MenuShortcut对象,用于指定快捷键。使用虚拟代码而不是字符。如Ctrl+A:

  1. MenuShortcut ms = new MenuShortcut(KeyEvent.VK_A);
  2. //如果还需要Shift
  3. MenuShortcut ms = new MenuShortcut(KeyEvent.VK_A, true);
  4. //创建exitItem菜单项,指定用Ctrl+X快捷键
  5. MenuItem exitItem = new MenuItem("退出", new MenuShortcut(KeyEvent.VK_X));

菜单分组可用菜单分隔线,有两种方法添加:

  • 调用Menu对象的addSeparator()方法
  • 添加new MenuItem("-")

创建了MenuItem、Menu和MenuBar对象后,调用Menu的add()方法将多个MenuItem组合成菜单(也可将另一个Menu对象组合进来,从而形成二级菜单),再调用MenuBar的add()方法将多个Menu组合成菜单条,最后调用Frame对象的setMenuBar()方法为该窗口添加菜单条。

  1. Frame f = new Frame("Test");
  2. MenuBar mb = new MenuBar();
  3. //为f窗口设置菜单条
  4. f.setMenuBar(mb);

AWT的菜单组件不能创建图标菜单,如需图标则应使用Swing的菜单组件:JMenuBar、JMenu、JmenuItem和JPopupMenu组件。


右键菜单

即PopupMenu对象。步骤如下:

  • 创建PopupMenu实例
  • 创建多个MenuItem实例,依次将这些实例加入PopupMenu中
  • 将PopupMenu加入到目标组件中
  • 为需要出现上下文菜单的组件编写鼠标监听器,当用户释放鼠标右键时弹出右键菜单
  1. PopupMenu pop = new PopupMenu();
  2. Panel p = new Panel();
  3. p.add(pop);
  4. //添加鼠标事件监听器
  5. p.addMouseListener(new MouseAdapter(){
  6. public void mouseReleased(MouseEvent e){
  7. //如果释放的是鼠标右键
  8. if ( e.isPopupTrigger()){
  9. pop.show(p, e.getX(), e.getY());
  10. }
  11. }
  12. });

AWT并没有为GUI组件提供实现,它仅仅是调用运行平台的GUI组件来创建和平台一致的对等体。因此程序中的TextArea实际上是Windows的多行文本域组件的对等体,具有和它相同的行为,所以TextArea默认就具有右键菜单。


在AWT中绘图

画图的实现原理

Component类提供了和绘图有关的三个方法:

  • paint(Graphics g):绘制组件的外观
  • update(Graphics g):调用paint()方法,刷新组件外观
  • repaint():调用update()方法,刷新组件外观

上面三个方法依次是后者调用前者。
Container类中的update()方法先以组件的背景色填充整个组件区域,然后调用paint()方法重画组件。
Container类的update()方法代码如下:

  1. public void update(Graphics g){
  2. if (isShowing()){
  3. //以组件的背景色填充整个组件区域
  4. if (!(peer instanceof LightweightPeer)){
  5. g.clearRect(0, 0, width, height);
  6. }
  7. paint(g);
  8. }
  9. }

普通组件的update()方法则直接调用paint()方法。

程序不应该主动调用组件的paint()和update()方法,它们都由AWT系统负责调用。如果程序希望AWT系统重新绘制该组件,则调用该组件的repaint()方法即可。而paint()和update()方法通常被重写。
重写update()或paint()方法时,该方法里包含了一个Graphics类型的参数。通过该参数可以实现绘图功能。


使用Graphics类

Graphics是一个抽象的画笔对象,提供了如下方法用于绘制几何图形和绘图

  • drawLine():直线
  • drawString():字符串
  • drawRect():矩形
  • drawRoundRect():圆角矩形
  • drawOval():椭圆
  • drawPolygon():多边形边框
  • drawArc():圆弧(可以是椭圆的圆弧)
  • drawPolyline():折线
  • fillRect():填充矩形
  • fillRoundRect():填充圆角矩形
  • fillOval():填充椭圆区域
  • fillPolygon():填充多边形区域
  • fillArc():填充圆弧和圆弧两个端点到中心连线所包围的区域
  • drawImage():绘制位图
  • setColor():设置画笔颜色(仅绘制字符串时有效)
  • setFont():设置画笔的字体(仅绘制字符串时有效)

AWT普通组件也可以通过Color()和Font()方法来改变它的前景色和字体。所有组件都有一个setBackground()方法用于设置组件的背景色

AWT专门提供了一个Convas类作为绘图的画面,程序可以通过创建Canvas的子类,并重写它的paint()方法来实现绘图。

  1. import java.awt.*;
  2. import java.util.*;
  3. public class Test {
  4. MyCanvas drawArea = new MyCanvas();
  5. Frame f = new Frame("画图");
  6. public void init(){
  7. Panel p = new Panel();
  8. drawArea.setPreferredSize(new Dimension(250,180));
  9. f.add(drawArea);
  10. f.add(p, BorderLayout.SOUTH);
  11. f.pack();
  12. f.setVisible(true);
  13. drawArea.repaint();//repaint()会自动调用 paint()方法
  14. }
  15. public static void main(String[] args) {
  16. new Test().init();
  17. }
  18. class MyCanvas extends Canvas {
  19. //重写Canvas的paint()方法,实现绘画
  20. public void paint(Graphics g) {
  21. Random rand = new Random();
  22. //设置画笔颜色
  23. g.setColor(new Color(220, 100, 80));
  24. //随机绘制一个矩形框
  25. g.drawRect(rand.nextInt(200), rand.nextInt(120), 40, 60);
  26. }
  27. }
  28. }

Java也可开发一些动画,即间隔一定时间(通常小于0.1秒)重新绘制新的图像,两次绘制的图像之间的差异很小,肉眼看起来就成了所谓的动画。为了实现间隔一定的时间就重新调用组件的repaint()方法,可以借助于Swing类提供的Timer类。
Timer类是一个定时器,有如下构造器:

  • Timer(int delay, ActionListener listener):每间隔delay毫秒,系统自动触发ActionListener监听器里的一鸣惊人监听器大(actionPerformed()方法)。

下面程序示范一个简单的弹球游戏,其中小球和球拍分别以圆形区域和矩形区域代替,小球开始以随机速度向下运动,遇到边框或球拍时小球反弹;球拍则由用户控制,

  1. import java.awt.*;
  2. import java.awt.event.ActionEvent;
  3. import java.awt.event.ActionListener;
  4. import java.awt.event.KeyAdapter;
  5. import java.awt.event.KeyEvent;
  6. import java.util.Random;
  7. import javax.swing.Timer;
  8. public class Test {
  9. //桌面的宽度和高度
  10. private final int TABLE_WIDTH = 300;
  11. private final int TABLE_HEIGHT = 400;
  12. //球拍的垂直位置
  13. private final int RACKET_Y = 340;
  14. //球拍的高度和宽度
  15. private final int RACKET_HEIGHT = 20;
  16. private final int RACKET_WIDTH = 60;
  17. //小球的大小
  18. private final int BALL_SIZE = 16;
  19. private Frame f = new Frame("弹球游戏");
  20. Random rand = new Random();
  21. //小球纵向运行速度
  22. private int ySpeed = 10;
  23. //返回一个-0.5~0.5的比率,用于控制小球的方向
  24. private double xyRate = rand.nextDouble() - 0.5;
  25. //小球横向运行速度
  26. private int xSpeed = (int) (ySpeed * xyRate * 2);
  27. //小球坐标
  28. private int ballX = rand.nextInt(200) + 20;
  29. private int ballY = rand.nextInt(10) + 20;
  30. //rackedX代表球拍的水平位置
  31. private int racketX = rand.nextInt(200);
  32. private MyCanvas tableArea = new MyCanvas();
  33. Timer timer;
  34. //游戏是否结束的旗标
  35. private boolean isLose = false;
  36. public void init() {
  37. //设置桌面区域的最佳大小
  38. tableArea.setPreferredSize(new Dimension(TABLE_WIDTH, TABLE_HEIGHT));
  39. f.add(tableArea);
  40. //定义键盘监听器
  41. KeyAdapter keyProcessor = new KeyAdapter() {
  42. public void keyPressed(KeyEvent ke) {
  43. //按下向左、向右键时,球拍水平坐标分别减少、增加
  44. if (ke.getKeyCode() == KeyEvent.VK_LEFT) {
  45. if (racketX > 0) {
  46. racketX -= 10;
  47. }
  48. }
  49. if (ke.getKeyCode() == KeyEvent.VK_RIGHT) {
  50. if (racketX < TABLE_WIDTH - RACKET_WIDTH) {
  51. racketX += 10;
  52. }
  53. }
  54. }
  55. };
  56. //为窗口和tableArea对象分别添加键盘监听器
  57. f.addKeyListener(keyProcessor);
  58. tableArea.addKeyListener(keyProcessor);
  59. //定义每0.1秒执行一次的事件监听器
  60. ActionListener taskPerformer = new ActionListener() {
  61. public void actionPerformed(ActionEvent evt) {
  62. //如果小球碰到左边框、右边框
  63. if (ballX <= 0 || ballX >= TABLE_WIDTH - BALL_SIZE) {
  64. xSpeed = -xSpeed;
  65. }
  66. //如果小球高度超出了球拍位置,且横向不在球拍范围之内,游戏结束
  67. if (ballY >= RACKET_Y - BALL_SIZE && (ballX < racketX || ballX > racketX + RACKET_WIDTH)) {
  68. timer.stop();
  69. //设置游戏是否结束的旗标为true
  70. isLose = true;
  71. tableArea.repaint();
  72. }
  73. //如果小球到顶部或者位于球拍之内,且达到球拍位置,小球反弹
  74. else if (ballY <= 0 || (ballY >= RACKET_Y - BALL_SIZE && ballX > racketX && ballX <= racketX + RACKET_WIDTH)) {
  75. ySpeed= -ySpeed;
  76. }
  77. //小球坐标增加
  78. ballY += ySpeed;
  79. ballX += xSpeed;
  80. tableArea.repaint();
  81. }
  82. };
  83. timer = new Timer(100, taskPerformer);
  84. timer.start();
  85. f.pack();
  86. f.setVisible(true);
  87. }
  88. public static void main(String[] args) {
  89. new Test().init();
  90. }
  91. class MyCanvas extends Canvas{
  92. //重写Canvas的paint()方法,实现绘画
  93. public void paint(Graphics g) {
  94. //如果游戏已经结束
  95. if (isLose) {
  96. g.setColor(new Color(255, 0, 0));
  97. g.setFont(new Font("Times", Font.BOLD, 30));
  98. g.drawString("游戏已结束!", 50, 200);
  99. }
  100. //如果游戏未结束
  101. else {
  102. //设置颜色并绘制小球
  103. g.setColor(new Color(240, 240, 80));
  104. g.fillOval(ballX, ballY, BALL_SIZE, BALL_SIZE);
  105. //设置颜色
  106. g.setColor(new Color(80, 80, 200));
  107. g.fillRect(racketX, RACKET_Y, RACKET_WIDTH, RACKET_HEIGHT);
  108. }
  109. }
  110. }
  111. }

游戏有轻微闪烁,这是由于AWT组件的绘图没有采用双缓冲技术,当重写paint()方法来绘制图形时,所有图形都是直接绘制到GUI组件上的,所以多次调用paint()进行绘制会发生闪烁现象。使用Swing组件就可避免这种闪烁。Swing组件没有提供Canvas对应的组件,使用Swing的Panel组件作为画布即可。


处理位图

AWT允许在组件上绘制位图,Graphics提供了drawImage方法用于绘制位图,该方法需要一个Image参数代表位图。


Image抽象类和BufferedImage实现类

BufferedImage是一个可访问图像缓冲区的Image实现类,构造器:

  • BufferedImage(int width, int height, int imageType):imageType可以是BufferedImage.TYPE_INT_RGB、BufferedImage.TYPE_BYTE_GRAY等值。

BufferedImage还提供了一个getGrapthics()方法返回对象的Graphics对象,从而允许通过该Graphics对象向Image中添加图形。
借助BufferedImage的帮助,可以在AWT中实现缓冲技术——先将图形绘制到BufferedImage对象中,再调用组件的drawImage方法一次性地将BufferedImage对象绘制到特定组件上。


使用ImageIO输入/输出位图

ImageIO利用ImageReader和ImageWriter读写图形文件。

  • static String[] getReaderFileSuffixes():返回ImageIO所有能读的图形文件后缀
  • static String[] getReaderFormatNames():返回ImageIO所有能读的图形文件的非正式格式名称
  • static String[] getWriterFileSuffixes()
  • static String[] getWriterFormatNames() 同上
  1. Image image = ImageIO.read(new File("image/board.jpg"));
  2. ImageIO.write(image, "jpeg", new File(System.currentTimeMillis() + ".jpg"));

剪贴板

AWT支持两种剪贴板:本地剪贴板和系统剪贴板。
本地剪贴板:适用同一虚拟机的不同窗口。与运行平台无关,可传输任意格式。
系统剪贴板:适用不同虚拟机或Java与第三方程序之间。


数据传递的类和接口

AWT中剪贴板相关操作的接口和类被放在java.awt.datetransfer包下。

  • Clipboard:一个剪贴板实例,系统或本地剪贴板
  • ClipboardOwner:剪贴板内容的所有者接口,当剪贴板内容的所有权被修改时,系统将会触发该所有者的lostOwnership事件处理器
  • Transferable:放进剪贴板中的传输对象。
  • DateFlavor:用于表述剪贴板中的数据格式。
  • StringSelection:接口Transferable的实现类,用于传输文本字符串
  • FlavorListener:数据格式监听器接口
  • FlavorEvent:封装了数据格式改变的事件

传递文本

步骤如下:
<1>创建一个Clipboard实例。

  1. //创建系统剪贴板
  2. Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
  3. //创建本地剪贴板:
  4. Clipboard clipboard = new Clipboard("cb");

<2>将需要放入剪贴板中的字符串封装成StringSelection对象

  1. StringSelection st = new StringSelection(targetStr);

<3>调用剪贴板对象的setContents()方法将StringSelection放进剪贴板中,该方法有两个参数,Transferable和ClipboardOwner对象,通常把第二个参数设为null

  1. clipboard.setContents(st, null);

从剪贴板中取出数据,调用Clipboard对象的getDate(DataFlavor flavor)方法。如果指定flavor的数据不存在,该方法将引发UnsupportedFlavorException异常。为免出现异常,可以先调用Clipboard对象的isDataFlavorAvailable(DataFlavor flavor)来判断指定flavor的数据是否存在。
getData方法返回的是Object类型,需强制转型。

  1. if (clipboard.isDateFlavorAvailable(DateFlavor.stringFlavor)){
  2. String content = (String)clipboard.getData(DateFlavor.stringFlavor);
  3. }

用系统剪贴板传递图像

要将图像放入剪贴板内,则必须提供一个Transferable接口的实现类。该实现类封装一个image对象,并且向外表现为imageFlavor内容

  1. public class ImageSelection implements Transferable{
  2. private Image image;
  3. //构造器
  4. public ImageSelection(Image image){
  5. this.image = image;
  6. }
  7. //返回该Transferable对象所支持的所有DateFlavor
  8. public DateFlavor[] getTransferDataFlavors(){
  9. return new DataFlavor[]{DataFlavor.imageFlavor};
  10. }
  11. //取出该Transferable对象里实际的数据
  12. public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException{
  13. if (flavor.equals(DataFlavor.imageFlavor)){
  14. return image;
  15. }
  16. else{
  17. throw new UnsupportedFlavorException(flavor);
  18. }
  19. }
  20. //返回该Transferable对象是否支持指定的DataFlavor
  21. public boolean isDataFlavorSupported(DataFlavor flavor){
  22. return flavor.equals(DataFlavor.imageFlavor);
  23. }
  24. }

有了以上ImageSelection封装类后,可以用下面的语句操作Image对象放入和取出剪贴板

  1. //将image对象封装成ImageSelection对象
  2. ImageSelection contents = new ImageSelection(image);
  3. //将ImageSelection对象放入剪贴板中
  4. clipboard.setContents(contents, null);
  5. //取出
  6. if (clipboard.isDataFlavorAvailable(DataFlavor.imageFlavor)){
  7. try{
  8. //取出剪贴板中的imageFlavor内容
  9. myImage =(Image)clipboard.getData(DataFlavor.imageFlavor));
  10. }
  11. catch (Exception e){
  12. e.printStackTrace();
  13. }
  14. }

使用本地剪贴板传递对象引用

本地剪贴板可以保存任何类型的Java对象。为了将任意类型的Java对象保存到剪贴板中,DataFlavor里提供了一个javaJVMLocalObjectMimeType的常量,该常量 是一个MIME类型字符串:application/x-java-jvm-local-objectref,将Java对象放入本地剪贴板中必须使用该MIME类型。该MIME类型表示仅将对象引用复制到剪贴板中,对象引用只有在同一个虚拟机中才有效。
Java并没有提供封装对象引用的Transferable实现类,因此必须自己实现该接口。

  1. public class LocalObjectSelection implements Transferable{
  2. //持有一个对象的引用
  3. private Object obj;
  4. //构造器
  5. public LocalObjectSelection(Object obj){
  6. this.obj = obj
  7. }
  8. //返回该Transferable对象支持的DataFlavor
  9. public DataFlavor[] getTransferDataFlavors(){
  10. DataFlavor[] flavors = new DataFlavor[2];
  11. //获取被封装对象的类型
  12. Class clazz = obj.getClass();
  13. String mimeType = "application/x-java-jvm-local-objectref;" + "class=" + clazz.getName();
  14. try{
  15. flavors[0] = new DataFlavor(mimeType);
  16. flavors[1] = DataFlavor.stringFlavor;
  17. return flavors;
  18. }
  19. catch (ClassNotFoundException e){
  20. e.printStackTrace();
  21. return null;
  22. }
  23. }
  24. //取出该Transferable对象封装的数据
  25. public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException{
  26. if(!isDataFlavorSupported(flavor)){
  27. throw new UnsupportedFlavorException(flavor);
  28. }
  29. if(flavor.equals(DataFlavor.stringFlavor)){
  30. return obj.toString();
  31. }
  32. return obj;
  33. }
  34. public boolean isDataFlavorSupported(DataFlavor flavor){
  35. return flavor.equals(DataFlavor.stringFlavor) || flavor.getPrimaryType().equals("application") && flavor.getSubType().equals("x-java-jvm-local-objectref") && flavor.getRepresentationClass().isAssignableFrom(obj.getClass());
  36. }
  37. }

上面程序创建了一个DataFlavor对象,用于表示本地Person对象引用的数据格式。创建DataFlavor对象可以使用如下构造器:

  1. DataFlavor(String mimeType):根据mimeType字符串构造DataFlavor

有了上面的LocalObjectSelection封装类后,就可以使用该封装类来封装某个对象的引用,从而将该对象的引用放入本地剪贴板中。

  1. Person p = new Person(name, age);
  2. //将Person对象封装成LocalObjectSelection对象
  3. LocalObjectSeletion ls = new LocalObjectSelection(p);
  4. //将LocalObjectSelection对象放入本地剪贴板中
  5. clipboard.setContents(ls, null);
  6. //取出
  7. //创建保存Person对象引用的DataFlavor对象
  8. DataFlavor personFlavor = new DataFlavor("application/x-java-jvm-local-objectref;class=Person");
  9. //取出本地剪贴板中的内容
  10. if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)){
  11. Person p = (Person)clipboard.getData(personFlavor);
  12. }

通过系统剪贴板传递Java对象

系统剪贴板支持传输序列化的Java对象和远程对象,复制到剪贴板中的序列化的Java对象和远程对象可以使用另一个Java程序(不在同一个虚拟机内的程序)来读取。DataFlavor中提供了javaSerializedObjectMimeType、javaRemoteObjectMimeType两个字符串常量来表示序列化的Java对象和远程对象的MIME类型,这两种MIME对象提供了复制对象、读取对象所包含的复杂操作,程序只需创建对应的Transferable实现类即可。
如果某个类是可序列化的,则该类的实例可以转换成二进制流,从而可以将该对象通过网络传输或保存到磁盘上,为了保证某个类是可序列化的,只要让该类实现Serializable接口即可。

  1. public class SerialSelection implements Transferable{
  2. //持有一个可序列化的对象
  3. private Serializable obj;
  4. //创建该类的对象时传入被持有的对象
  5. public SerialSelection(Serializable obj){
  6. this.obj = obj;
  7. }
  8. public DataFlavor[] getTransferDataFlavor(){
  9. DataFlavor[] flavors = new DataFlavor[2];
  10. //获取被封装对象的类型
  11. Class clazz = obj.getClass();
  12. try{
  13. flavors[0] = new DataFlavor(DataFlavor.javaSerializedObjectMimeType + ";class=" + clazz.getName());
  14. flavors[1] = DataFlavor.stringFlavor;
  15. return flavors;
  16. }
  17. catch (ClassNotFoundException e){
  18. e.printStackTrace();
  19. return null;
  20. }
  21. }
  22. public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException{
  23. if(!isDataFlavorSupported(flavor)){
  24. throw new UnsupportedFlavorException(flavor);
  25. }
  26. if(flavor.equals(DataFlavor.stringFlavor)){
  27. return obj.toString();
  28. }
  29. return obj;
  30. }
  31. public boolean isDataFlavorSupported(DataFlavor flavor){
  32. return flavor.equals(DataFlavor.stringFlavor) || flavor.getPrimaryType().equals("application") && flavor.getSubType().equals("x-java-serialized-object") && flavor.getRepresentationClass().isAssignableFrom(obj.getClass());
  33. }
  34. }

拖放功能

AWT提供了对拖放源和拖放目标的支持,分别由DragSource和DropTarget两个类来表示。
拖放操作中被传递的目标也用Transferable接口来封装,同样使用DataFlavor来表示被传递的数据格式


拖放目标

AWT提供了DropTarget类来表示拖放目标,通过该类的构造器来创建一个拖放目标:

  • DropTarget(Component c, int ops, DropTargetListener dtl):将组件c创建成一个拖放目标,默认可接受ops值所指定的拖放操作。

ops可接受的值和监听器的事件处理器详见API

下面程序利用拖放目标创建了一个简单的图片浏览工具,当用户把一个或多个图片文件拖入该窗口时,该窗口将会自动打开每个图片文件

  1. import javax.swing.*;
  2. import java.awt.*;
  3. import java.awt.datatransfer.DataFlavor;
  4. import java.awt.datatransfer.Transferable;
  5. import java.awt.dnd.DnDConstants;
  6. import java.awt.dnd.DropTargetAdapter;
  7. import java.awt.dnd.DropTargetDragEvent;
  8. import java.io.IOException;
  9. public class DropTargetTest{
  10. final int DESKTOP_WIDTH = 480;
  11. final int DESKTOP_HEIGHT = 360;
  12. final int FRAME_DISTANCE = 30;
  13. JFrame jf = new JFrame("测试拖放目标——把图片文件拖入该窗口");
  14. //定义一个虚拟桌面
  15. private JDesktopPane desktop = new JDesktopPane();
  16. //保存下一个内部窗口的坐标点
  17. private int nextFrameX;
  18. private int nextFrameY;
  19. //定义内部窗口为虚拟桌面的1/2大小
  20. private int width = DESKTOP_WIDTH / 2;
  21. private int height = DESKTOP_HEIGHT / 2;
  22. public void init(){
  23. desktop.setPreferredSize(new Dimension(DESKTOP_WIDTH, DESKTOP_HEIGHT));
  24. //将当前窗口创建成拖放目标
  25. new DropTarget(jf, DnDConstants.ACTION_COPY, new ImageDropTargetListener());
  26. jf.add(desktop);
  27. jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  28. jf.pack();
  29. jf.setVisible(true);
  30. }
  31. class ImageDropTargetListener extends DropTargetAdapter {
  32. public void drop(DropTargetirDragEvent event) {
  33. //接受复制操作
  34. event.acceptDrag(DnDConstants.ACTION_COPY);
  35. //获取拖放内容
  36. Transferable transferable = event.getTransferable();
  37. DataFlavor[] flavors = transferable.getTransferDataFlavors();
  38. //遍历拖放内容里的所有数据格式
  39. for (int i = 0; i < flavors.length; i++){
  40. DataFlavor d = flavors[i];
  41. try {
  42. //如果拖放内容的数据格式是文件列表
  43. if (d.equals(DataFlavor.javaFileListFlavor)) {
  44. //取出拖放操作里的文件列表
  45. List fileList = (List) transferable.getTransferData(d);
  46. for (Object f : fileList) {
  47. //显示每个文件
  48. showImage((File) f, event);
  49. }
  50. }
  51. }
  52. catch (Exception e) {
  53. e.printStackTrace();
  54. }
  55. //强制拖放操作结束,停止阻塞拖放目标
  56. event.dropComplete(true);
  57. }
  58. }
  59. }
  60. //显示每个文件的工具方法
  61. private void showImage(File f , DropTargetDragEvent event) throws IOException {
  62. Image image = ImageIO.read(f);
  63. if (image == null) {
  64. //强制拖放操作结束,停止阻塞拖放目标
  65. event.dropComplete(true);
  66. JOptionPane.showInternalMessageDialog(desktop, "系统不支持这种类型的文件");
  67. //方法返回,不会继续操作
  68. return;
  69. }
  70. ImageIcon icon = new ImageIcon(image);
  71. //创建内部窗口显示该图片
  72. JInternalFrame iframe = new JInternalFrame(f.getName(), true, true, true, true);
  73. JLabel imageLabel = new JLabel(icon);
  74. iframe.add(new JScrollPane(imageLabel));
  75. desktop.add(iframe);
  76. //设置内部窗口的原始位置(内部窗口默认大小是0x0,放在0,0位置)
  77. iframe.reshape(nextFrameX, nextFrameY, width, height);
  78. //使该窗口可见,并尝试选中它
  79. iframe.show();
  80. //计算下一个内部窗口的位置
  81. nextFrameX += FRAME_DISTANCE;
  82. nextFrameY += FRAME_DISTANCE;
  83. if (nextFrameX + width > desktop.getWidth()) {
  84. nextFrameX = 0;
  85. }
  86. if (nextFrameY + height > desktop.getHeight()) {
  87. nextFrameY = 0;
  88. }
  89. }
  90. public static void main(String[] args) {
  91. new DropTargetTest().init();
  92. }
  93. }

文本格式的拖放内容使用DataFlavor.stringFlavor格式来表示。
带格式的内容处理方法如下

  1. //如果被拖放的内容是text/html格式的输入流
  2. if ( d.isMimeTypeEqual("text/html") && d.getRepresentationClass() == InputStream.class){
  3. String charset = d.getParameter("charset");
  4. InputStreamReader reader = new InputStreamReader(transferable.getTransferData(d), charset);
  5. //使用IO流读取拖放操作的内容
  6. ...
  7. }

拖放源

创建拖放源的步骤如下:
<1>调用DragSource的getDefaultDragSource()方法获得与平台关联的DragSource对象。
<2>调用DragSource对象的createDefaultDragGestureRecognizer(Component c, int action, DragGestureListener dgl)方法将指定组件转换成拖放源。
如下代码将会把一个JLabel对象转换为拖放源

  1. //将srcLabel组件转换为拖放源
  2. dragSource.createDefaultDragGestureRecognizer(srcLabel, DnDconstants.Action_COPY_OR_MOVE, new MyDragGestureListener());

<3>为第2步中的DragGestureListener监听器提供实现类,该实现类需要重写接口里包含的dragGestureRecognized()方法,该方法负责把拖放内容封装成Transferable对象
下面程序示范了如何把一个JLabel转换成拖放源

  1. import javax.swing.*;
  2. import java.awt.*;
  3. import java.awt.datatransfer.StringSelection;
  4. import java.awt.datatransfer.Transferable;
  5. import java.awt.dnd.DnDConstants;
  6. import java.awt.dnd.DragGestureEvent;
  7. import java.awt.dnd.DragGestureListener;
  8. import java.awt.dnd.DragSource;
  9. public class DragSourceTest{
  10. JFrame jf = new JFrame("Swing的拖放支持");
  11. JLabel srcLabel = new JLabel("AWT的拖放支持.\n" + "将该文本域的内容拖入其他程序.\n");
  12. public void init(){
  13. DragSource dragSource = DragSource.getDefaultDragSource();
  14. //将srcLabel转换成拖放源,它能接受复制、移动两种操作
  15. dragSource.createDefaultDragGestureRecognizer(srcLabel, DnDConstants.ACTION_COPY_OR_MOVE, new DragGestureListener() {
  16. @Override
  17. public void dragGestureRecognized(DragGestureEvent event) {
  18. //将Jlabel里的文本信息包装成Transferable对象
  19. String txt = srcLabel.getText();
  20. Transferable transferable = new StringSelection(txt);
  21. //继续拖放操作,拖放过程中使用手状光标
  22. event.startDrag(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR), transferable);
  23. }
  24. });
  25. jf.add(new JScrollPane(srcLabel));
  26. jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  27. jf.pack();
  28. jf.setVisible(true);
  29. }
  30. public static void main(String[] args) {
  31. new DragSourceTest().init();
  32. }
  33. }

Swing编程


Swing概述

实际使用Java开发图形界面程序时,绝大部分时候都是用Swing组件开发的。
Swing采用100%的Java实现,不再依赖于本地平台的图形界面。
Swing组件都采用MVC(Model-View-Controller,即模型——视图——控制器)设计模式,从而可以实现GUI组件的显示逻辑和数据逻辑的分离,允许程序员自定义Render来改变GUI组件的显示外观,提供更多的灵活性。Model用于维护组件的各种状态,View是组件的可视化表现,Controller用于控制对于各种事件、组件做出怎样的响应。当模型发生改变时,它会通知所有依赖它的视图,视图会根据模型数据来更新自己。Swing使用UI代理来包装视图和控制器,还有另一个模型对象来维护该组件的状态。Swing组件的模型是自动设置的,对于简单的组件无须关心Model对象,因此Swing的MVC实现也被称为Model-Delegate(模型-代理)。
当组件的外观被改变时,对组件的状态信息(由模型维护)没有任何影响。因此,Swing可以使用插拔式外观感觉(Pluggable Look And Feel,PLAF)来控制组件外观,使得Swing图形界面在同一个平台上运行时能拥有不同的外观,用户可以自由选择。
Swing提供了多种独立于各种平台的LAF(Look And Feel),默认是一种名为Metal的LAF,这种LAF吸收了Macintosh平台的风格,显得较漂亮。Java 7则提供了一种名为Nimbus的LAF,更漂亮。
为了获取当前JRE所支持的LAF,可以借助于UIManager的getInstalledLookAndFeels()方法

  1. import javax.swing.UIManager;
  2. public class Test {
  3. public static void main(String[] args) {
  4. for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
  5. System.out.println("●" + info.getName() + ":\n" + info);
  6. }
  7. }
  8. }

除此之外还有大量的Java爱好者提供了各种开源的LAF。


Swing基本组件的用法

Swing为所有的AWT组件提供了对应实现(除了Canvas组件之外,因为在Swing中无须继承Canvas组件),通常在AWT组件的组件名前添加“J”就变成了对应的Swing组件。


Java 7 的Swing组件层次

大部分Swing组件都是JComponent抽象类的直接或间接子类。JComponent类是AWT里java.awt.Container类的子类。绝大部分Swing组件类继承了Container类,所以Swing组件都可作为窗口使用。
例外:

  • JComboBox:对应AWT的Choice组件,功能更丰富
  • JFileChooser:对应AWT里的FileDialog组件
  • JScrollBar:对应AWT的Scrollbar组件,注意b大小写区别
  • JCheckBox,对应AWT的Checkbox组件,注意b大小写区别
  • JCheckBoxMenuItem:对应AWT的CheckboxMenuItem组件,注意b大小写区别

上面的b主要是早期Java命名不规范造成的。
Swing中包含了4个组件直接继承了AWT组件,而不是从JComponent派生的:JFrame、JWindow、JDialog和JApplet,它们并不是轻量级组件,而是重量级组件(需要部分委托给运行平台上GUI组件的对等体)。

将Swing组件安功能分:

  • 顶层容器:JFrame、JApplet、JDialo、JWindow
  • 中间容器:JPanel、FScrollPane、JSplitPane、JToolBar等
  • 特殊容器:在用户界面上具有特殊作用的中间容器,如JInternalFrame、JRootPane、JLayeredPane和JDestopPane等
  • 基本组件:实现人机交互的组件,如JButton、JComboBox、JList、JMenu、JSlider等。
  • 不可编辑信息的显示组件:JLabe、JProgressBar和JToolTip等
  • 可编辑信息的显示组件:JTable、JTextArea和JTextField等
  • 特殊对话框组件:JColorChooser和JFileChooser等

AWT组件的Swing实现

第个Swing组件都有一个对应的UI类。类名总是将J去掉,然后在后面添加UI后缀。UI代理类通常是一个抽象基类,不同的PLAF会有不同的UI代理实现类(也称PLAF实现)。
如需改变程序的外观风格:

  1. try{
  2. //设置使用Motif风格
  3. UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
  4. //通过更新f容器以及f容器里所有组件的UI
  5. SwingUtilities.updateComponentTreeUI(f);
  6. }
  7. catch(Exception e){
  8. e.PrintStackTrace();
  9. }

下面用Swing创建窗口应用

  1. import javax.swing.*;
  2. import java.awt.*;
  3. import java.awt.event.ActionEvent;
  4. import java.awt.event.ActionListener;
  5. import java.awt.event.InputEvent;
  6. public class SwingComponent {
  7. JFrame f = new JFrame("测试");
  8. JButton ok = new JButton("确认");
  9. //定义单选按钮
  10. JRadioButton male = new JRadioButton("男", true);
  11. JRadioButton female = new JRadioButton("女", false);
  12. //ButtonGroup组合上面两个JRadioButton
  13. ButtonGroup bg = new ButtonGroup();
  14. //复选框
  15. JCheckBox married = new JCheckBox("是否已婚?", false);
  16. String[] colors = new String[]{"红色", "绿色", "蓝色"};
  17. //下拉选择框
  18. JComboBox<String> colorChooser = new JComboBox<String>(colors);
  19. //列表选择框
  20. JList<String> colorList = new JList<String>(colors);
  21. //多行文本域
  22. JTextArea ta = new JTextArea(8, 20);
  23. //单行文本域
  24. JTextField name = new JTextField(40);
  25. JMenuBar mb = new JMenuBar();
  26. JMenu file = new JMenu("文件");
  27. JMenu edit = new JMenu("编辑");
  28. JMenuItem newItem = new JMenuItem("新建");
  29. JMenuItem saveItem = new JMenuItem("保存");
  30. JMenuItem exitItem = new JMenuItem("退出");
  31. JCheckBoxMenuItem autoWrap = new JCheckBoxMenuItem("自动换行");
  32. JMenuItem copyItem = new JMenuItem("复制");
  33. JMenuItem pasteItem = new JMenuItem("粘贴");
  34. JMenu format = new JMenu("格式");
  35. JMenuItem commentItem = new JMenuItem("注释");
  36. JMenuItem cancelItem = new JMenuItem("取消注释");
  37. //右键菜单
  38. JPopupMenu pop = new JPopupMenu();
  39. //组合3个风格菜单项
  40. ButtonGroup flavorGroup = new ButtonGroup();
  41. //5个单选按钮,设定程序外观
  42. JRadioButtonMenuItem metalItem = new JRadioButtonMenuItem("Metal风格", true);
  43. JRadioButtonMenuItem nimbusItem = new JRadioButtonMenuItem("Nimbus风格");
  44. JRadioButtonMenuItem windowsItem = new JRadioButtonMenuItem("Windows风格");
  45. JRadioButtonMenuItem classicItem = new JRadioButtonMenuItem("Windows经典风格");
  46. JRadioButtonMenuItem motifItem = new JRadioButtonMenuItem("Motif风格");
  47. public void init() {
  48. //JPanel装载文本框、按钮
  49. JPanel bottom = new JPanel();
  50. bottom.add(name);
  51. bottom.add(ok);
  52. f.add(bottom, BorderLayout.SOUTH);
  53. //JPanel装载下拉选择框,三个JcheckBox
  54. JPanel checkPanel = new JPanel();
  55. checkPanel.add(colorChooser);
  56. //把两个JRadioButton加入ButtomGroup,使它们无法被同时选中。
  57. bg.add(male);
  58. bg.add(female);
  59. //把两个JradioBUtton加入JFrame,使它们能在界面上显示
  60. checkPanel.add(male);
  61. checkPanel.add(female);
  62. checkPanel.add(married);
  63. //垂直Box盛装多行文本域JPanel
  64. Box topLeft = Box.createVerticalBox();
  65. //使用JScrollPane作为普通组件的JViewPort,为了让多行文本域具有滚动条,将它放下JScrollPane容器中
  66. JScrollPane taJsp = new JScrollPane(ta);//TA是多行文本域
  67. topLeft.add(taJsp);
  68. topLeft.add(checkPanel);
  69. //水平Box盛装topLeft、colorList
  70. Box top = Box.createHorizontalBox();
  71. top.add(topLeft);
  72. top.add(colorList);
  73. f.add(top);
  74. //下面开始组合菜单,并添加监听器
  75. //设置快捷键
  76. newItem.setAccelerator(KeyStroke.getKeyStroke('N', InputEvent.CTRL_DOWN_MASK));
  77. newItem.addActionListener(new ActionListener() {
  78. public void actionPerformed(ActionEvent e) {
  79. ta.append("用户单击了“新建”菜单");
  80. }
  81. });
  82. file.add(newItem);
  83. file.add(saveItem);
  84. file.add(exitItem);
  85. edit.add(autoWrap);
  86. //添加菜单分隔线
  87. edit.addSeparator();
  88. edit.add(copyItem);
  89. edit.add(pasteItem);
  90. //为commentItem组件添加提示信息,鼠标移上去会有文字提示
  91. commentItem.setToolTipText("将程序代码注释起来");
  92. format.add(commentItem);
  93. format.add(cancelItem);
  94. //使用添加new JMenuItem("-")的方式不能添加菜单分隔符
  95. edit.add(new JMenuItem("-"));
  96. //format菜单组合到edit菜单中,形成二级菜单
  97. edit.add(format);
  98. //file、edit菜单添加到mb菜单条中
  99. mb.add(file);
  100. mb.add(edit);
  101. f.setJMenuBar(mb);
  102. //组合右键菜单
  103. //flavorGroup是ButtonGroup,将5个选项组合在一起,使其只能五选一
  104. flavorGroup.add(metalItem);
  105. flavorGroup.add(nimbusItem);
  106. flavorGroup.add(windowsItem);
  107. flavorGroup.add(classicItem);
  108. flavorGroup.add(motifItem);
  109. //添加到右键菜单
  110. pop.add(metalItem);
  111. pop.add(nimbusItem);
  112. pop.add(windowsItem);
  113. pop.add(classicItem);
  114. pop.add(motifItem);
  115. //为5个风格菜单创建事件监听器
  116. ActionListener flavorListener = new ActionListener() {
  117. public void actionPerformed(ActionEvent e) {
  118. try {
  119. switch (e.getActionCommand()) {
  120. case "Metal风格":
  121. changeFlavor(1);
  122. break;
  123. case "Nimbus风格":
  124. changeFlavor(2);
  125. break;
  126. case "Windows风格":
  127. changeFlavor(4);
  128. break;
  129. case "Windows经典风格":
  130. changeFlavor(5);
  131. break;
  132. }
  133. }
  134. catch (Exception ee) {
  135. ee.printStackTrace();
  136. }
  137. }
  138. };
  139. //为5个风格菜单项添加事件监听器
  140. metalItem.addActionListener(flavorListener);
  141. nimbusItem.addActionListener(flavorListener);
  142. windowsItem.addActionListener(flavorListener);
  143. classicItem.addActionListener(flavorListener);
  144. motifItem.addActionListener(flavorListener);
  145. motifItem.addActionListener(flavorListener);
  146. //调用该方法即可设置右键菜单,无须使用事件机制
  147. ta.setComponentPopupMenu(pop);//ta是多行文本域
  148. //设置关闭窗口时,退出程序
  149. f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  150. f.pack();
  151. f.setVisible(true);
  152. }
  153. //定义一个方法,用于改变界面风格
  154. private void changeFlavor(int flavor) throws Exception{
  155. switch (flavor){
  156. case 1:
  157. UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
  158. break;
  159. case 2:
  160. UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel");
  161. break;
  162. case 3:
  163. UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
  164. break;
  165. case 4:
  166. UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel");
  167. break;
  168. case 5:
  169. UIManager.setLookAndFeel("com.sun.java.swimg.plaf.motif.MotifLookAndFeel");
  170. break;
  171. }
  172. //更新f窗口内顶级容器以及内部所有组件的UI,注意这里更新的不是JFrame本身,详细介绍见下文
  173. SwingUtilities.updateComponentTreeUI(f.getContentPane());
  174. //更新mb菜单条内部所有组件的UI
  175. SwingUtilities.updateComponentTreeUI(mb);