@king
2015-02-03T10:10:15.000000Z
字数 172562
阅读 6969
Java
cmd /k javac -encoding utf-8 "$(FULL_CURRENT_PATH)" & java -classpath "$(CURRENT_DIRECTORY)" "$(NAME_PART)" & PAUSE & EXIT
- 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。
public class Person
{
//下面定义一个初始化块
{
int a = 6;
if (a > 4)
{
System.out.println("Person初始化块:局部变量a的值大于4");
}
System.out.println("Person的初始化块");
}
//定义第二个初始化块
{
System.out.println("Person的第二个初始化块");
}
//定义无参数的构造器
public Person()
{
System.out.println("Person类的无参数构造器");
}
public static void main(String[] args)
{
new person();
}
}
上面程序的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)
8种基本数据类型不支持面向对象的编程机制,基本数据类型的数据也不具备“对象”的特性:没有变量、方法可以被调用。包装类可使8种基本数据类型的变量当成Object类型变量使用。
基本数据类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
char | Character |
float | Float |
double | Double |
boolean | Boolean |
//把一个字符串转换成Boolean对象
Boolean bObj = new Boolean("false");
当试图使用一个字符串来创建Boolean对象时,如果传入的字符串是'true',或是此字符串不同字母的大小写变化形式,如'True',都将创建true对应的Boolean对象;如果传入其他字符串,则会创建false对应的Boolean对象
如果希望获得包装类对象中包装的基本类型变量,则可以使用包装类提供的xxxValue()实例方法。
从JDK 1.5 之后提供了自动装箱Autoboxing和自动拆箱AutoUnboxing功能。
自动装箱是指可以把一个基本类型变量直接赋给对应的包装类变量,或者赋给Object变量(Object是所有类的父类,子类对象可以直接赋给父类变量)
自动拆箱则与之相反
//直接把一个boolean类型变量赋给一个Object类型变量
Object boo1Obj = true;
if (boo1Obj instanceof Boolean )
{
//先把Object对象强制类型转换为Boolean类型,再赋给boolean变量
boolean b = (Boolean)boo1Obj;
}
包装类还可实现基本类型变量和字符串之间的转换。
把字符串类型的值转换为基本类型的值有两种方式:
- 利用包装类提供的parseXxx(String s)静态方法(Character除外)
- 利用包装类提供的Xxx(String s)构造器
String类提供了多个重载valueOf()方法,用于将基本类型变量转换成字符串。
String intStr = "123";
//把一个特定字符串转换成int变量
int it1 = Integer.parseInt(intStr);
int it2 = new Integer(intStr);
String floatStr = "4.56";
//把一个特定字符串转换成float变量
float ft1 = Float.parseFloat(floatStr);
float ft2 = new Float(floatStr);
//把一个浮点变量转换成String变量
String ftStr = String.valueOf(2.345f);
String dbStr = String.valueOf(3.344);
//把一个boolean变量转换成String变量
String boolStr = String.valueOf(true);
//把基本类型变量转换成字条串更简单的方法
String intStr = 5 + "";
Java 7 为所有的包装类提供了一个静态的compare(xxx val1, xxx val2)方法,这样开发者可以比较两个基本类型值的大小,包括两个boolean类型的值(true>false)
基本类型的数据成员的默认初始值:
数值型: 0
布尔值: false
其他类型: null
多个有意义的单词连缀而成,第一个单词首字母小写,后面每个单词首字母大写
如果在定义方法时,在最后一个形参的类型后增加三点(...),则表明该形参可以接受多个参数值,多个参数值被当成数组传入。
public class Varargs
{
//定义了形参个数可变的方法
public static void test(int a, String... books)
{
//book被当成数组处理
for(String tmp: books)
{
System.out.println(tmp);
}
System.out.println(a);
}
public static void main(String[] args)
{
test(5, "疯狂Java讲义", "轻量级JavaEE企业应用实战");//传给books参数的实参数值无须是一个数组
}
}
如果用数组形参来定义:
public static void test(int a, String[] books);
那么调用test时必须传给该形参一个数组:
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开始。其他类型的数组与整数数组相似。
type[][] = new type[length][];
type[][] = new type[length1][length2];
使用new type[length]
初始化一维数组后,相当于定义了length个type类型的变量;类似的,使用new type[length][]
初始化这个数组后,相当于定义了length个type[]类型的变量。
两个boolean值的与、或、非的逻辑关系
true && false and
(3 > 1) || (2 == 1) or
!true not
对整数的二进制形式逐位进行逻辑运算,得到一个整数
& and
| or
^ xor
~ not
5 << 3 0b101 left shift 3 bits
6 >> 1 0b110 right shift 1 bit
还有下列在C中常见的运算符,我会在用到的时候进一步解释:
m++ 变量m加1
n-- 变量n减1
condition ? x1 : x2 condition为一个boolean值。根据condition,取x1或x2的值
Java中控制结构(control flow)的语法与C类似。它们都使用{}
来表达隶属关系。
if
if (conditon1) {
statements;
...
}
else if (condition2) {
statements;
...
}
else {
statements;
...
}
上面的condition是一个表示真假值的表达式。statements;
是语句。
1.while
while (condition) {
statements;
}
2.do... while
do {
statements;
} while(condition); // 注意结尾的【;】
3.for
for (initial; condition; update) {
statements;
}
或者
for(String name:nameArray){}//逐个运行数组或其他集合的元素
4.跳过或跳出循环
在循环中,可以使用
break; // 跳出循环
continue; // 直接进入下一环
4.foreach循环
循环遍历数组或集合
for (type variableName:array|collection)
{
statements;
}
封装(encapsulation)是计算机常见的术语,即保留有限的外部接口(interface),隐藏具体实施细节。比如在Linux架构,就可以看到Linux操作系统封装了底层硬件的具体细节,只保留了系统调用这一套接口。用户处在封装的外部,只能通过接口,进行所需的操作。提高易用性、安全性。
Java通过三个关键字来控制对象的成员的外部可见性(visibility): public
, private
, protected
。
public
: 该成员外部可见,即该成员为接口的一部分
private
: 该成员外部不可见,只能用于内部使用,无法从外部访问。
(protected
涉及继承的概念,放在以后说)
内部方法并不受封装的影响,外部方法只能调用public
成员。
在Java的通常规范中,表达状态的数据成员(比如height)要设置成private
。对数据成员的修改要通过接口提供的方法进行(比如getHeight()和growHeight())。这个规范起到了保护数据的作用。用户不能直接修改数据,必须通过相应的方法才能读取和写入数据。类的设计者可以在接口方法中加入数据的使用规范。
在一个.java文件中,有且只能有一个类带有public关键字。
访问控制符 | private | default | protected | public |
---|---|---|---|---|
同一个类中 | √ | √ | √ | √ |
同一个包中 | √ | √ | √ | |
子类中 | √ | √ | ||
全局范围内 | √ |
修饰符 | 外部类/接口 | 成员属性 | 方法 | 构造器 | 初始化块 | 成员内部类 | 局部成员 |
---|---|---|---|---|---|---|---|
public | √ | √ | √ | √ | √ | ||
protected | √ | √ | √ | √ | |||
包访问控制符 | √ | √ | √ | √ | 不能使用任何控制符 | √ | 不能使用任何控制符 |
private | √ | √ | √ | √ | |||
abstract | √ | √ | √ | ||||
final | √ | √ | √ | √ | √ | ||
static | √ | √ | √ | √ | |||
strictfp | √ | √ | √ | ||||
synchronized | √ | ||||||
native | √ | ||||||
transient | √ | ||||||
volatile | √ |
extends
,表示对父类的继承,可以实现父类,也可以调用父类初始化 this.parent()。而且会覆盖父类定义的变量或者函数。这样的好处就是:架构师定义好接口,让工程师实现就可以了。整个项目开发效率得到提高,且开发成本大大降低。
implements
,实现父类,子类不可以覆盖父类的方法或者变量。即使子类定义与父类相同的变量或者函数,也会被父类取代掉。
以杯子为例,定义一个杯子的接口:
interface Cup {
void addWater(int w);
void drinkWater(int w);
}
Cup这个interface
中定义了两个方法的原型(stereotype): addWater()和drinkWater()。一个方法的原型规定了方法名,参数列表和返回类型。原型可以告诉外部如何使用这些方法。
在interface中,我们
不需要定义方法的主体
不需要说明方法的可见性
注意第二点,interface
中的方法默认为public
。
我们用implements
关键字来实施interface。一旦在类中实施了某个interface,必须在该类中定义interface的所有方法。类中的方法需要与interface中的方法原型相符。否则,Java将报错。在类中可以定义interface没有提及的其他public方法。
class MusicCup implements Cup {
public void addWater(int w){
this.water = this.water + w;
}
public void drinkWater(int w){
this.water = this.water - w;
}
public int waterContent(){
return this.water;
}
private int water = 0;
}
我们使用了interface,但这个interface并没有减少我们定义类时的工作量。我们依然要像之前一样,具体的编写类。我们甚至于要更加小心,不能违反了interface的规定。既然如此,我们为什么要使用interface呢?
事实上,interface就像是行业标准。一个工厂(类)可以采纳行业标准 (implement interface),也可以不采纳行业标准。但是,一个采纳了行业标准的产品将有下面的好处:
更高质量: 没有加水功能的杯子不符合标准。
更容易推广: 正如电脑上的USB接口一样,下游产品可以更容易衔接。
class MusicCup implements MusicPlayer, Cup
在一个正常的Java项目中,我们往往需要编写不止一个.java程序,最终的Java产品包括了所有的Java程序。因此,Java需要解决组织Java程序的问题。包(package)的目的就是为了更好的组织Java程序。
在Java程序的开始加入package就可以了。包名全部小写
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文件夹)位于当前的工作路径中,那么不需要特别的设置,就可以使用包了
import com.vamei.society.*;
public class TestAgain{
public static void main(String[] args){
Human aPerson = new Human(180);
System.out.println(aPerson.getHeight());
}
}
import
用于识别路径。利用import
语句,我们可以引入相应路径下的类。*表示引入society文件夹下的所有类,不包括子文件夹
我们也可以提供类的完整的路径。这可以区分同名但不同路径的类,比如:
public class TestAgain{
public static void main(String[] args) {
com.vamei.society.Human aPerson =
new com.vamei.society.Human(180);
System.out.println(aPerson.getHeight());
}
}
由于我们提供了完整的类路径,所以不需要使用import语句。
包管理的是.class文件。
- 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.导入指定类的单个静态数据成员或方法
//导入java.lang.System类的out静态数据成员
import static java.lang.System.out;
2.导入类的全部静态数据成员或方法
//导入java.lang.System类的全部静态数据成员、方法
import static java.lang.System.*;
继承(inheritance)是面向对象的重要概念。继承是除组合(composition)之外,提高代码重复可用性(reusibility)的另一种重要方式。我们在组合(composition)中看到,组合是重复调用对象的功能接口。继承可以重复利用已有的类的定义。
我们用extends
关键字表示继承:
class Woman extends Human
通过继承,我们创建了一个新类,叫做衍生类(derived class)。被继承的类(Human)称为基类(base class)。衍生类以基类作为自己定义的基础,并补充基类中没有定义的giveBirth()方法。
构造器不能继承。
如果定义一个Java类时并未显式指定这个类的直接父类,则这个类默认扩展java.lang.Object类。所以所有的Java对象都可以调用java.lang.Object类的实例方法。
判断前面的对象是否是后面的类
//判断myObject是否String类对象
boolean judge1= myObject instanceof String;
修改基类成员的方法。比如,在衍生层,也就是定义Woman时,可以修改基类提供的breath()方法:
class Woman extends Human{
public Human giveBirth(){
System.out.println("Give birth");
return (new Human(20));
}
public void breath(){
super.breath();
System.out.println("su...");
}
}
如果父类方法的访问权限是private,则该方法对子类是隐藏的,其子类既无法访问也无法重写该方法。如果子类中定义了一个与父类private方法具有相同的方法名、相同的形参列表、相同的返回值类型的方法,依然不是重写,只是在子类中重新定义了一个新方法。
*重载主要发生在同一个类的多个同名方法之间,参数列表不同
*重写发生在子类和父类的同名方法之间,参数列表相同
*父类方法和子类方法也可能发生重载
我们要在衍生类的定义中定义与类同名的构造方法。在该构造方法中:
比如下面的程序中,Human类有一个构造方法:
class Human{
public Human(int h){
this.height = h;
}
……
}
衍生类Woman类的定义及其构造方法:
class Woman extends Human{
public Woman(int h){
super(h); // base class constructor
System.out.println("Hello, Pandora!");
}
……
}
不管我们是否使用super调用来执行父类构造器的初始化代码,子类构造器总会先调用父类构造器一次。依此类推,创建任何Java对象,最先执行的总是java.lang.Object类的构造器。
在一个新类的定义中使用其他对象。这就是组合(composition)。组合是在Java中实现程序复用(reusibility)的基本手段之一。
类与类继承:
class Animal
{
private void beat()
{
System.out.println("心脏跳动...");
}
public void breath()
{
beat();
System.out.println("吸一口气,吐一口气,呼吸中...");
}
}
//继承Animal,直接复用父类的breath方法
class Bird extends Animal
{
public void fly
{
System.out.println("我在天空自由地飞翔");
}
}
public class Test
{
public static void main(String[] args)
{
Bird b = new Bird();
b.breath();
b.fly();
}
}
将上述程序改写为组合
class Animal
{
private void beat()
{
System.out.println("心脏跳动...");
}
public void breath()
{
beat();
System.out.println("吸一口气,吐一口气,呼吸中...");
}
}
class Bird
{
//将原来的父类嵌入原来的子类,作为子类的一个组合成分
private Animal a;
public Bird(Animal a)
{
this.a = a;
}
//重新定义一个自己的breath方法
public void breath()
{
//直接复用Animal提供的breath方法来实现Bird的breath方法
a.breath();
}
public void fly
{
System.out.println("我在天空自由地飞翔");
}
}
public class Test
{
public static void main(String[] args)
{
//此时需要显式创建被嵌入的对象
Animal a1 = new Animal();
Bird b = new Bird(a1);
b.breath();
b.fly();
}
}
有一些数据用于表述类的状态。比如Human类,我们可以用“人口”来表示Human类的对象的总数。“人口”直接描述类的状态,而不是某个对象。
类的所有对象共享“人口”数据。这样的数据被称为类数据成员(class field)。
在类定义中,我们利用static关键字,来声明类数据成员,比如:
如:
class Human{
public Human(int h){
this.height = h;
}
public int getHeight() {
return this.height;
}
public void growHeight(int h){
this.height = this.height + h;
}
public void breath(){
System.out.println("hu...hu...");
}
private int height;
private static int population;
public static boolean is_mammal = true;
}
我们定义了两个类数据成员: 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方法中,将没有隐式传递的this和super参数。我们无从引用属于对象的数据和方法(这正是我们想要的效果)。类方法中,不能访问对象的数据。
下面我们增加一个static方法getPopulation()
,该方法返回static数据population:
class Human{
//……
public static int getPopulation(){
return Human.population;
}
//……
}
调用类方法时,我们可以通过class.method()的方式调用,也可以通过object.method()的方式调用。比如使用下面的Test类测试:
public class Test{
public static void main(String[] args){
System.out.println(Human.getPopulation());
Human aPerson = new Human(160);
System.out.println(aPerson.getPopulation());
}
}
我们通过两种方式,在类定义的外部调用了类方法getPopulation()。
我们看到,对象方法可以访问类数据。这是非常有用的概念。类的状态有可能随着对象而发生变化。比如“人口”,它应该随着一个对象的产生而增加1。我们可以在对象的方法中修改类的“人口”数据。我们下面在构造方法中访问类数据成员。这里的构造方法是非static的方法,即对象的方法:
class Human{
public Human(int h) {
this.height = h;
Human.populatin = Human.population + 1;//注意这里
}
//……
private static int population;
private static boolean is_mammal = true;
}
当我们每创建一个对象时,都会通过该对象的构造方法修改类数据,为population类数据增加1。这样,population就能实时的反映属于该类的对象的总数
除了上面举的构造方法的例子,我们也可以在普通的对象方法中访问类数据。
final关键字的基本含义是: 这个数据/方法/类不能被改变了。
final基本类型的数据: 定值 (constant value),只能赋值一次,不能再被修改。
final方法: 该方法不能被覆盖。private的方法默认为final的方法。
final类: 该类不能被继承。
普通类型的对象也可以有final关键字,它表示对象引用(reference)不能再被修改。即该引用只能指向一个对象。但是,对象的内容可以改变 (类似于C中的static指针)。我们将在以后介绍对象引用。
如果一个基本类型的数据既为final,也是static,那么它是只存储了一份的定值。这非常适合于存储一些常量,比如圆周率。
在实施接口中,我们利用interface语法,将interface从类定义中独立出来,构成一个主体。interface为类提供了接口规范。
在继承中,我们为了提高程序的可复用性,引入的继承机制。当时的继承是基于类的。interface接口同样可以继承,以拓展原interface。
接口继承(inheritance)与类继承很类似,就是以被继承的interface为基础,增添新增的接口方法原型。比如,我们以Cup作为原interface:
interface Cup {
void addWater(int w);
void drinkWater(int w);
}
我们在继承Cup的基础上,定义一个新的有刻度的杯子的接口,MetricCup,接口如下:
interface MetricCup extends Cup{
int WaterContent();
}
我们增添了一个新的方法原型WaterContent(),这个方法返回一个整数(水量)。
在Java类的继承中,一个衍生类只能有一个基类。也就是说,一个类不能同时继承多于一个的类。在Java中,interface可以同时继承多于一个interface,这叫做多重继承(multiple inheritance)。
比如我们有下面一个Player接口:
interface Player{
void play();
}
我们新增一个MusicCup的接口。它既有Cup接口,又有Player接口,并增加一个display()方法原型。
interface MusicCup extends Cup, Player {
void display();
}
(如何使用interface,见实施接口)
在生活中,我们会有一些很抽象的概念。这些抽象的概念往往是许多类的集合,比如:
粮食 (可以是玉米、小麦、大米)
图形 (可以是三角形、圆形、正方形)
人类 (可以是男人、女人)
在组织这样的关系时,我们可以使用继承。根据我们的常识:
"Food类的对象"的说法是抽象的。这样一个对象应该是属于Corn, Rice, Wheat子类中的一个。
Food类有eat()方法 (食物可以吃)。然而,这样的一个动作是抽象的。粮食的具体吃法是不同的。比如Corn需要剥皮吃,Wheat要磨成面粉吃。我们需要在每个类中覆盖Food类的eat()方法。
Java中提供了抽象类abstract class
的语法,用于说明类及其方法的抽象性。比如
abstract class Food {
public abstract void eat();
public void happyFood();
{
System.out.println("Good! Eat Me!");
}
}
类中的方法可以声明为abstract
,比如上面的eat()。这时,我们不需要具体定义方法,只需要提供该方法的原型。这与接口类似。当我们在比如Corn类中继承该类时,需要提供eat()方法的具体定义。
当一个类中出现abstract方法时,这个类的声明必须加上abstract关键字,否则Java将报错。一个abstract类不能用于创建对象。
我们可以像继承类那样继承一个抽象类。我们必须用完整的方法定义,来覆盖抽象类中的抽象方法,否则,衍生类依然是一个抽象类。
抽象类的定义中可以有数据成员。数据成员的继承与正常类的继承相同。
外部可以调用类来创建对象,比如:
Human aPerson = new Human(160);
创建了一个Human类的对象aPerson。
上面是一个非常简单的表述,但我们有许多细节需要深入:
栈的读取速度比堆快,但栈上存储的数据受到有效范围的限制。在C语言中,当一次函数调用结束时,相应的栈帧(stack frame)要删除,栈帧上存储的参量和自动变量就消失了。Java的栈也受到同样的限制,当一次方法调用结束,该方法存储在栈上的数据将清空。在 Java中,所有的(普通)对象都储存在堆上。因此,new关键字的完整含义是,在堆上创建对象。
基本类型(primitive type)的对象,比如int, double,保存在栈上。当我们声明基本类型时,不需要new。一旦声明,Java将在栈上直接存储基本类型的数据。所以,基本类型的变量名表示的是数据本身,不是引用。
Java可以对基本类型的变量进行类型转换。不同的基本类型有不同的长度和存储范围。如果我们从一个高精度类型转换到低精度类型,比如从float转换到int,那么我们有可能会损失信息。这样的转换叫做收缩变换(narrowing conversion)。这种情况下,我们需要显示的声明类型转换,比如:
public class Test{
public static void main(String[] args){
int a;
a = (int) 1.23; // narrowing conversion
System.out.println(a);
}
}
如果我们从低精度类型转换成高精度类型,则不存在信息损失的顾虑。这样的变换叫做宽松变换(widening conversion)。我们不需要显示的要求类型转换,Java可以自动进行。
将一个衍生类引用转换为其基类引用,这叫做向上转换(upcast)或者宽松转换。下面的BrokenCup类继承自Cup类,并覆盖了Cup类中原有的addWater()和drinkWater()方法:
public class Test{
public static void main(String[] args){
Cup aCup;
BrokenCup aBrokenCup = new BrokenCup();
aCup = aBrokenCup; // upcast
aCup.addWater(10); // method binding
}
}
class Cup{
public void addWater(int w){
this.water = this.water + w;
}
public void drinkWater(int w){
this.water = this.water - w;
}
private int water = 0;
}
class BrokenCup extends Cup{
public void addWater(int w) {
System.out.println("shit, broken cup");
}
public void drinkWater(int w) {
System.out.println("om...num..., no water inside");
}
}
程序运行结果:
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()方法,这个方法接收一个杯子对象和一个整数作为参数。整数表示喝水的水量:
public class Test{
public static void main(String[] args){
Human guest = new Human();
BrokenCup hisCup = new BrokenCup();
guest.drink(hisCup, 10);
}
}
class Human{
void drink(Cup aCup, int w){
aCup.drinkWater(w);
}
}
程序运行结果:
shit, no water inside
我们在Human类的drink()的定义中,要求第一个参量为Cup类型的引用。但在实际运用时(Test类),将Cup的BrokenCup衍生类对象。这实际上是将hisCup向上转型称为Cup类,传递给drink()方法。在方法中,我们调用了drinkWater()方法。Java发现这个对象实际上是BrokenCup对象,所以实际调用了BrokenCup的相应方法。
我们可以将一个基类引用向下转型(downcast)成为衍生类的引用,但要求该基类引用所指向的对象,已经是所要downcast的衍生类对象。比如可以将上面的hisCup向上转型为Cup类引用后,再向下转型成为BrokenCup类引用。
Java中,所有的类实际上都有一个共同的继承祖先,即Object类。Object类提供了一些方法,比如toString()。我们可以在自己的类定义中覆盖这些方法。
String类包含在java.lang包中。这个包会在Java启动的时候自动import,所以可以当做一个内置类(built-in class)。我们不需要显式的使用import引入String类。
创建String类对象不需要new关键字。比如:
public class Test{
public static void main(String[] args){
String s = "Hello World!";
System.out.println(s);
}
}
实际上,当你写出一个"Hello World"表达式时,内存中就已经创建了该对象。如果使用new String("Hello World!"),会重复创建出一个字符串对象。String类是唯一一个不需要new关键字来创建对象的类。
可以用+实现字符串的连接(concatenate),比如:
"abc" + s
字符串的操作大都通过字符串的相应方法实现,比如下面的方法:
方法 效果
s.length() //返回s字符串长度
s.charAt(2) //返回s字符串中下标为2的字符
s.substring(0, 4) //返回s字符串中下标0到4的子字符串
s.indexOf("Hello") //返回子字符串"Hello"的下标
s.startsWith(" ") //判断s是否以空格开始
s.endsWith("oo") //判断s是否以"oo"结束
s.equals("Good World!") //判断s是否等于"Good World!" ,"=="只能判断字符串是否保存在同一位置。需要使用equals()判断字符串的内容是否相同。
s.compareTo("Hello Nerd!") //比较s字符串与"Hello Nerd!"在词典中的顺序,返回一个整数,如果<0,说明s在"Hello Nerd!"之前;如果>0,说明s在"Hello Nerd!"之后;如果==0,说明s与"Hello Nerd!"相等。
s.trim() //去掉s前后的空格字符串,并返回新的字符串
s.toUpperCase() //将s转换为大写字母,并返回新的字符串
s.toLowerCase() //将s转换为小写,并返回新的字符串
s.replace("World", "Universe") //将"World"替换为"Universe",并返回新的字符串
String类对象是不可变对象(immutable object)。程序员不能对已有的不可变对象进行修改。我们自己也可以创建不可变对象,只要在接口中不提供修改数据的方法就可以。
然而,String类对象确实有编辑字符串的功能,比如replace()。这些编辑功能是通过创建一个新的对象来实现的,而不是对原有对象进行修改。比如:
s = s.replace("World", "Universe");
右边对s.replace()的调用将创建一个新的字符串"Hello Universe!",并返回该对象(的引用)。通过赋值,引用s将指向该新的字符串。如果没有其他引用指向原有字符串"Hello World!",原字符串对象将被垃圾回收。
enum
定义、非抽象的枚举类默认会使用final
修饰,因此枚举类不能派生子类。private
。public static final
修饰,无须显式添加
//创建枚举类
public enum Gender{
//此处的枚举值必须调用对应的构造器来创建,无须new关键字,也无须显式调用构造器
Male("男"),FEMALE("女");
private final String name;
//枚举类的构造器只能用private修饰
private Gender(String name){
this.name = name;
}
public String getName(){
return this.name;
}
}
public class GenderTestP{
public static void main(String[] args){
//通过Enum的valueOf方法来获取指定枚举类的枚举值
Gender g = Enum.valueOf(Gender.class , "FEMALE");
}
}
java.lang.Enum类中提供了如下几个方法:
- int compareTo(E o):该方法用于与指定枚举对象比较顺序。只能与相同类型的枚举实例进行比较。
- String name():返回此枚举实例的名称。
- int ordinal():返回枚举值在枚举类中的索引值(即枚举值在枚举声明中的位置,第一个枚举值的索引值为零)
- String toString():返回枚举常量的名称,与name方法相似但更常用
- public static > T valueOf(class enumType,String name):返回指定枚举类中指定名称的枚举值
枚举类也可以一个或多个接口。
public interface GenderDesc{
void info();//抽象方法
}
public enum Gender implements GenderDesc{
//其他部分与上面的Gender类完全相同
……
//增加下面的info方法,实现GenderDesc接口必须实现的
public void info(){
System.out.println("输出详细情况");
}
}
如果需要每个枚举值在调用该方法时呈现出不同的行为方式,则可以使用匿名内部子类
public enum Gender implements GenderDesc{
//此处的枚举值必须调用对应的构造器创建
//花括号部分实际上是一个类体部分
MALE("男"){
public void info(){
System.out.println("男性");
}
}
FEMALE("女"){
public void info(){
System.out.println("女性");
}
}
//枚举类的其他部分与上面的Gender类完全相同
……
}
非抽象的枚举类默认使用final修饰,但是上面的类继承了GenderDesc接口,包含了抽象方法,那么它是抽象枚举类,可以派生子类。
Scanner主要提供了两个方法来扫描输入:
- hasNextXxx():是否还有下一个输入项,其中Xxx可以是Int、Long等代表基本数据类型的字符串。如果需要判断是否包含下一个字符串,则可以省去Xxx。
- nextXxx():获取下一个输入项。
默认情况下,Scanner使用空白(包括空格、Tab空白、回车)作为多个输入项之间的分隔符。
Scanner sc = new Scanner(System.in);//System.in代表标准输入,就是键盘输入
//以下代码指定只把回车作为分隔符
sc.useDelimiter("\n");//其中的参数是一个正则表达式
Scanner提供了两个简单的方法来逐行读取:
- boolean hasNextLine():返回输入源中是否还有下一行。
- String nextLine():返回输入源中下一行的字符串。
创建Scanner对象时传入一个File对象作为参数
//将一个File对象作为Scanner的构造器参数,Scanner读取文件内容
Scanner sc = new Scanner(new File("ScannerFileTest.java"));
//判断是否还有下一行
while(sc.hasNextLine()){
//输出文件的下一行
System.out.println(sc.nextLine);
}
Scanner是Java 5 新增的工具类。在这之前程序常通过BufferedReader类来读取键盘输入。
BufferedReader是Java IO 流中的一个字符、包装流,它必须建立在另一个字符流的基础之上。但标准输入System.in是字节流,程序需要使用转换流InputStreamReader将其包装成字符流,所以程序中用于获取键盘输入的BufferedReader对象采用如下代码创建:
//以System.in字节流为基础,创建一个BufferedReader对象
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
//可以用readLine()方法来读取键盘输入
String line = null;
while((line = br.readLine()) != null){
System.out.println("用户键盘输入是:" + line);
}
使用BufferedReader可以逐行读取用户的键盘输入,每次键盘输入都被BufferedReader当成String对象,它不能读取基本类型输入项。
Java程序在不同操作系统上运行时,可能需要取得平台相关的属性,或者调用平台命令来完成特定功能。Java提供了System类和Runtime类来与程序的运行平台进行交互。
System类代表当前Java程序的运行平台,程序不能创建System类的对象,System类提供了一些类Field和类方法,允许直接通过System类来调用这些Field和方法。
System类提供了代表标准输入、标准输出和错误输出的类Field,并提供了一些静态方法用于访问环境变量、系统属性的方法,还提供了加载文件和动态链接库(loadLibrary..()方法)的方法。
//获取系统的所有环境变量,须先导入java.util.Map
Map<String,String> env = System.getenv();
for (String name : env.keySet()){
System.out.println(name + "--->" + env.get(name));
}
//获取指定环境变量的值
System.out.println(System.getenv("JAVA_HOME"));
//获取所有的系统属性,须先导入java.util.Properties
Properties props = System.getProperties();
//将所有的系统属性保存到props.txt文件中
props.store(new FileOutputStream("props.txt"), "System Properties");
//输出特定的系统特性
System.out.println(System.getProperty("os.name"));
//获取系统当前时间
long time1 = System.currentTimeMillis();//返回与1970年1月1日00:00:00的时间差,毫秒为单位
long time2 = System.currentTimeMillis();//返回与1970年1月1日00:00:00的时间差,纳秒为单位
//返回根据该对象的地址计算得到的精确hashCode值,若两对象的hashCode相同则为同一对象
String s = new String("Hello");
System.out.println(System.identityHashCode(s));
Runtime代表系统运行时环境,每个Java程序都有一个与之相对应的Runtime实例,应用程序通过该对象与其运行时环境相连。应用程序不能创建自己的Runtime实例,但可以通过getRuntime()方法获取与之相关联的Runtime对象。
//获取Java程序关联的运行时对象
Runtime rt = Runtime.getRuntime();
System.out.println("处理器数量:" + rt.availableProcessors());
System.out.println("空间内存数:" + rt.freeMemory());
System.out.println("总内存数:" + rt.totalMemory());
System.out.println("可用最大内存数:" + rt.maxMemory());
//单独启动一个进程来运行操作系统命令
//运行记事本程序,必须加throws Exception语句,否则编译不通过
public static void main(String[] args) throws Exception{
Runtime.getRuntime().exec("notepad");
}
- 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()方法来得到该对象的副本,并返回该副本。
class Address{
String detail;
public Address(String detail){
this.detail = detail;
}
}
//实现Cloneable接口
class User implements Cloneable{
int age;
Address address;
public User(int age){
this.age = age;
address = new Address("广州天河");
}
//通过调用super.clone()来实现clone()方法,User是返回类型,返回一个User对象
public User clone()
throws CloneNotSupportedException
{
return (User)super.clone();
}
}
public class CloneTest{
public static void main(String args[]) throws CloneNotSupportedException{
User u1 = new User(29);
//clone得到u1对象的副本
User u2 = u1.clone();
System.out.println(u1 == u2);//false
//clone只是简单复制引用变量,u1和u2的address都指向同一个字符串,所以下面输出true
//如果需要把引用变量指向的对象也克隆,则要自己开发者自己进行“递归”克隆
System.out.println(u1.address == u2.address);
}
}
注意不是Object!
Java为工具类命名习惯是添加一个字母s,比如操作数组的工具类是Arrays,操作集合的工具类是Collections。
public class ObjTest{
//定义一个obj变量,它的默认值是null;
static ObjTest obj;
public static void main(String args []){
//输出一个null对象的hashCode值,输出0
System.out.println(Objects.hashCode(obj));
//输出一个null对象的toString,输出null
System.out.println(Objects.toString(obj));
//要求obj不能为null,如果obj为null则引发异常,
System.out.println(Objects.requireNonNull(obj,"obj参数不能是null!"));
}
//Objects.requireNonNull()方法主要用于对方法形参进行输入校验,当参数不为null时返回参数本身,否则引发异常
public Foo(Bar bar){
//校验bar参数,如果bar参数为null将引发异常,否则this.bar被赋值为bar参数
}
}
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
Random类有两个构造器,一个使用默认种子(当前时间),另一个需要显式传入一个long整数。
ThreadLocalRandom类在并发访问的环境下,可以减少多线程竞争,最终保证系统具有良好的性能。
ThreadLocalRandom类的用法与Random类的用法基本相似,它提供了一个静态的current()方法来获取ThreadLocalRandom对象,之后可以调用各种nextXxx()方法来获取伪随机数了。
import java.util.random;
Random rand = new Random();
//生成随机布尔变量
boolean b = rand.nextBoolean();
//生成随机byte数组
byte[] b = new byte[16];
rand.nextBytes(b);
//生成0.0~1.0之间的伪随机double数
double b = rand.nextDouble();
//生成平均值是0.0,标准差是1.0的伪高斯数
double b = rand.nextGaussian();
//生成一个处于int整数聚范围的伪随机整数
int b = rand.nextInt();
//生成0~26之间的伪随机整数
int b = rand.nextInt(26);
//使用ThreadLocalRandom
ThreadLocalRandom rand = ThreadLocalRandom.current();
//生成4~20之间的伪随机整数
int b = rand.nextInt(4,20);
国际化Internationalization,简称I18N。
本地化Localization简称L10N。
Java程序国际化的思路是将程序中的标签、提示等信息放在资源文件中,程序需要支持哪些国家、语言环境,就对应提供相应的资源文件。资源文件是key-value对,每个资源文件中的key是不变的,但value则随不同的国家、语言而改变
java程序的国际化主要通过如下三个类完成:
- java.util.ResourceBundle:用于加载国家、语言资源包
- java.util.Locale:用于封装特定的国家/区域、语言环境
- java.text.MessageFormat:用于格式化带占位符的字符串
资源文件的命名可以有如下三种形式:
- baseName_language_country.properties
- baseName_language.properties
- baseName.properties
baseName用户随意指定;language和country都不可随意变化,必须是Java所支持的语言和国家
调用Locale类的getAvailableLocales()方法,返回一个Locale数组,包含了Java所支持的国家和语言。
import java.util.Locale;
//返回Java所支持的全部国家和语言的数组
Locale[] localeList = Locale.getAvailableLocales();
//遍历并输出
for(int i = 0; i < localeList.length; i++){
System.out.println(localeList[i].getDisplayCountry() + "=" + localeList[i].getCountry() + " " + localeList[i].getDisplayLanguage() + "=" + localeList[i].getLanguage());
}
为了保存数量不确定的数据,以及保存具有映射关系的数据(也被称为关联数组),Java提供了集合类。集合类主要负责保存、盛装其他数据,因此集合类又被称为容器类。所有集合类都处于java.util包下。后来为了处理多线程环境下的并发安全问题,Java 5 还在java.util.concurrent包下提供了一些多线程支持的集合类。
集合只能保存对象。数组可以保存基本类型+对象。
Set集合无序,元素不能重复。
List集合有序,元素可以重复,像一个长度可变的数组。
Map集合每项数据都由两个值组成,key不可重复,value可以重复。
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():把集合转换成一个数组,所有的集合元素变成对应的数组元素。
import java.util.Collection;
Collection c = new ArrayList();
//添加元素
c.add("孙悟空");
//虽然集合不能放基本类型的值,但Java支持自动装箱
c.add(6);
//删除指定元素
c.remove(6);
//判断是否包含指定字符串
boolean b = c.contains("孙悟空");
Iterator主要用于遍历(即迭代访问)Collection集合中的元素,必须领队于Collection对象。Iterator对象也被称为迭代器。定义了三个方法:
- boolean hasNext():如果被迭代的集合元素还没有被遍历完,则返回true
- Object next():返回集合里的下一个元素
- void remove():删除集合里上一次next方法返回的元素
import java.util.Iterator;
import java.util.Collection;
//创建一个集合
Collection books = new HashSet();
books.add("King");
books.add("Moon");
//获取books集合对应的迭代器
Iterator it = books.iterator();
while(it.hasNext()){
//it.next()方法返回的数据类型是Object类型,需要强制类型转换
String b = (String)it.next();
if(book.equals("King"){
//从集合中删除上一代next方法返回的元素
it.remove();
}
}
import java.util.Collection;
Collection books = new HashSet();
books.add("King");
books.add("Moon");
for (Object obj : books){
……
}
Set集合与Collection集合基本上完全一样,它没有提供任何额外的方法,只是行为略有不同(Set不允许包含重复元素)。
Set根据equals方法判断两个对象是否相同,而不是==运算符。
HashSet是Set接口的典型实现,大多数时候使用Set集合时就是使用这个实现类。HashSet按Hash算法来存储集合中的元素,因此具有很好的存取和查找性能
HashSet具有以下特点:
- 不能保证元素的排列顺序,顺序可能发生变化
- HashSet不是同步的,如果多个线程同时访问一个HashSet,假设有两个或两个以上线程同时修改了HashSet集合时,则必须通过代码来保证其同步
- 集合元素值可以是null
当向HashSet集合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到hashCode值,然后根据该值决定该对象在HashSet中的存储位置。如果有两个元素通过equals()方法比较返回true,但它们的hashCode()方法返回值不相等,HashSet将会把它们存储在不同的位置,依然可以添加成功。
Hash集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法返回值也相等。
HashSet还有一个子类LinkedHashSet,LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但它同时使用链表维护存储元素的次序,这样使元素看起来是以插入的顺序保存的。即遍历此集合元素时,将会按元素的添加顺序来访问集合里的元素。
用法与HashSet类似,输出集合元素时,元素的顺序问题与添加的顺序一致。依然不允许集合元素重复。
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集合关联
import java.util.TreeSet;
TreeSet ts = new TreeSet(new Comparator(){
public int compare(Object o1, Object o2){
……
}
})
EnumSet是专为枚举类设计的集合类,所有元素都必须是指定枚举类型的枚举值。EnumSet集合元素也是有序的,以枚举值在Enum类内的定义顺序来决定集合元素的顺序。
EnumSet在内部以位微量的形式存储,非常紧凑、高效,因此占用内存很小,而且运行效率很好。
EnumSet集合不允许加入null元素,否则会抛出异常
EnumSet类没有暴露任何构造器来创建该类的实例,程序应该通过它提供的static方法来创建EnumSet对象。详见API
Set的三个实现类HashSet、TreeSet和EnumSet都是线程不安全的。
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作为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
该方法对于基本数据类型的数组支持并不好,当数组是基本数据类型时不建议使用。
当使用asList()方法时,数组就和列表链接在一起了,当更新其中之一时,另一个将自动获得更新。注意:仅仅针对对象数组类型,基本数据类型数组不具备该特性
int[] a = { 1, 2, 3, 4 };
List aList = Arrays.asList(a);
System.out.println(aList);
预期输出应该是1,2,3,4,但实际上输出的仅仅是一个引用@hash值, 这里它把a当成了一个元素。
对象类型的数组使用asList,是我们预期的
Integer[] aInteger = new Integer[] { 1, 2, 3, 4 };
List aList = Arrays.asList(aInteger);
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保存队列元素按大小进行重新排序。因此当调用peek()方法或者polll()方法取出队列中的元素时,取出队列中最小的元素。
import java.util.PriorityQueue;
PriorityQueue pq = new PriorityQueue();
//加入加个元素
pq.offer(6);
pq.offer(-3);
pq.offer(9);
pq.offer(0);
//输出pq队列
System.out.println(pq); //[-3, 0, 9, 6],这是调用toString方法,返回值并没有很好地按大小顺序排序。
//访问队列的第一个元素,即最小值
System.out.println(pq.poll()); //-3
PriorityQueue的元素也有自然排序和定制排序,对元素的要求与TreeSet一致。
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,它是一个基于数组实现的双端队列。
import java.util.ArrayDeque;
ArrayDeque stack = new ArrayDeque();
//依次将三个元素push入栈
stack.push("King");
stack.push("Moon");
stack.push("Almoye");
//访问第一个元素但不将其pop出栈
System.out.println(Stack.peek());
……
程序中需要使用“栈”这种数据结构时,推荐使用ArrayDeque或LinkedList,而不是Stack
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的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的关系完全类似于ArrayList和Vector。
类似于HashSet,HashMap、Hashtable判断两个key相等的标准也是:两个key通过equals()方法比较返回true,两个key的hashCode值也相等。
另外,HashMap、Hashtable中还包含一个containsValue()方法,用于判断是否包含指定的value。判断标准是两个对象通过equals()方法返回true。
HashSet有一个子类是LinkedHashSet,HashMap也有一个LinkedHashMap子类,它也用双向链表来维护key-value对的次序(其实只需要考虑key的次序),该链表负责维护Map的迭代顺序,与key-value对的插入顺序保持一致。
LinkedHashMap可以避免对HashMap、Hashtable里的key-value对进行排序(只要插入key-value对时保持顺序即可),同时又可避免使用TreeMap所增加的成本。
LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能;但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。
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对输出到指定的属性文件(以输出流表示)中。
import java.util.Properties;
Properties props = new Propertis();
Properties props2 = new Properties();
//向Properties中添加属性
props.setProperty("username", "yeeku");
//将Properties中的key-value对保存到a.ini文件中
props.store(new FileOutputStream("a.ini"), "comment line");
//将a.ini文件中的key-value对追加到props2中
props2.load(new FileInputStream("a.ini"));
Properties还可以把key-value对以XML文件的形式保存起来,也可以从XML文件中加载key-value对,用法与此类似。
正如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与HashMap用法基本相似,区别在于HashMap的key保留了对实际对象的强引用,但WeakHashMap的key只保留了对实际对象的弱引用。
这个Map实现类的实现机制与HashMap基本相似,但它在处理两个key相等时比较独特:在IdentityHashMap中,当且仅当两个key严格相等(key1 == key2)时,IdentityHashMap才认为两个key相等;对于普通的HashMap而言,只要key1和key2通过equals()方法比较返回true,且它们的hashCode值相等即可
EnumMap所有key必须是单个枚举类的枚举值。创建EnumMap时必须显式或隐式指定它对应的枚举类。
EnumMap在内部以数组形式保存。
EnumMap根据key的自然顺序(即枚举值在枚举类中的定义顺序)来维护key-value对的顺序。
EnumMap不允许null作为key,但允许使用null作为value。
import java.util.EnumMap;
enum Season{
SPRING, SUMMER, FALL, WINTER
}
……
EnumMap enumMap = new EnumMap(Season.class);
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个元素“整体”移到前面;为负时相反。
import java.util.*;
ArrayList nums = new ArrayList();
nums.add(2);
nums.add(3);
//反转List
Collections.reverse(nums);
//自然排序排序
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都是线程不安全的。
import java.util.Collections;
Collection c = Collections.synchronizedCollection(new ArrayList());
List list = Collections.synchronizedList(new ArrayList());
Set s = Collections.synchronizedSet(new HashSet());
Collections提供了如下三类方法来返回一个不可变的集合,List、Set或Map。
- emptyXxx():返回一个空的、不可变的集合对象
- singletonXxx()返回一个只包含指定对象(只有一个若一项元素)的、不可变的集合对象
- unmodifiableXxx():返回指定集合对象的不可变视图。
在没有泛型之前,一旦把一个对象“丢进”Java集合中,集合就会忘记对象的类型,把所有的对象当成Object类型处理。当程序从集合中取出对象后,就需要进行强制类型转换,这种强制类型转换不仅使代码臃肿,而且容易引起ClassCastException异常
增加了泛型支持后的集合,完全可以记住集合中元素的类型,并可以在编译时检查集合中元素的类型,如果试图向集合中添加不满足类型要求的对象,编译器就会提示错误。增加泛型后的集合,可以让代码更加简洁,程序更加健壮。
Java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常。
Java集合缺点:把一个对象“丢进”集合,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就成了Object类型(其运行时类型没变)。
集合如此设计,原因是设计集合的程序员不会知道我们用它来保存什么类型的对象,这样带来两个问题:
- 集合对元素类型没有任何限制,所以对象都可以放进去
- 取出对象后需要进行强制类型转换,增加编程复杂度且会引起异常
List<String> strList = new ArrayList<String>();
此处的List是带一个类型参数的泛型接口。
Java 7 以前,如果使用带泛型的接口、类定义变量,那么调用构造器创建对象时,构造器的后面也必须带泛型,这显得多余了。
Java 7 开始,Java知构造器后不需要带完整的泛型信息,只要给出一对尖括号即可,Java可以推断尖括号里应该是什么泛型信息。
List<String> strList = new ArrayList<>;
由于<>像菱形,这种语法也被称为“菱形”语法。
所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。Java 5 必定了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。
下面是Java 5 必定后List接口、Iterator接口、Map的代码片段:
//定义接口时指定了一个类型形参,该形参名为E
public interface List<E>{
//在该接口里,E可作为类型使用
void add(E x);
Iterator<E> iterator();
……
}
//定义接口时指定了一个类型形参,该形参名为E
public interface Iterator<E>{
//在该接口里E完全可以作为类型使用
E next();
boolean hasNext();
……
}
//定义该接口时指定了两个类型形参,其形参名为K、V
public interface Map<K , V>{
//在该接口里K、V完全可以作为类型使用
Set<K> keySet();
V put(K key, V value)
……
}
虽然程序只定义了一个List接口,但实际使用时可以产生无数多个List接口,只要为E传入不同的类型实参,系统就会多出一个新的List接口。必须指出:List绝不会被替换成ListString,系统没有进行源代码复制,二进制代码中没有,磁盘中没有,内存中也没有。
包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数多个逻辑上的子类,但这种子类在物理上并不存在。
我们可以为任何类、接口增加泛型声明。
//定义Apple类时使用了泛型声明
public class Apple<T>{
//使用T类型形参定义实例变量
private T info;
public Apple(){}
//下面方法中使用T类型形参来定义构造器
public Apple(T info){
this.info = info;
}
public void setInfo(T info){
this.info = info;
}
public T getInfo(){
return this.info;
}
}
注意,当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不可增加泛型声明。
当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类,但需要指出的是,当使用这些接口、父类时不能带包含类型形参。例如下面的代码就是错误的:
public class A extends Apple<T>{}
方法中的形参代表变量、常量、表达式等数据,统称数据形参。定义方法时可以声明数据形参,调用方法时必须为这些数据形参传入实际的数据;与此类似的是,定义类、接口、方法时可以声明类型形参,使用类、接口、方法时应为类型形参传入实际的类型。
如果想从Apple类派生一个子类,则可以改为如下代码:
public class A extends Apple<String>
调用方法时必须为所有的数据形参传入参数值,与调用方法不同的是,使用类、接口时可以不为类型形参传入实际的类型参数(这是指紧接在类名后的形参),即下面的代码也是正确的
public class A extends Apple
如果从Apple类派生子类,则在Apple类中所有使用T类型形参的地方都将被替换成String类型,即它的子类将会继承到String getInfo()和void setInfo(String info)两个方法,如果子类要重写父类的方法就必须注意这一点。如下:
//继承上面的Apple类
public class A1 extends Apple<String>{
//正确重写了父类的方法、返回值,与父类Apple<String>的返回值完全相同
public String getInfo(){
return "子类" + super.getInfo();
}
}
如果使用Apple类时没有传入实际的类型参数,Java编译器可能发出警告:使用了未经检查或不安全的操作——这就是泛型检查的警告。
前面提到可以把ArrayList类当成ArrayList的子类,事实上,ArrayList类也确实像一种特殊的ArrayList类,这个ArrayList对象只能添加String对象作为集合元素。但实际上,系统并没有为ArrayList生成新的class文件,而且也不会把ArrayList当成新类来处理。
不管为泛型的类型开通传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只战胜一块内存空间,因此在静态方法、静态初始化块或静态变量的声明和初始化中不允许使用类型形参。
同样,系统并没有真正生成泛型类,instanceof运算符后不能使用泛型类。下面显示了如上错误:
//下面代码错误,不能在静态变量声明中使用类型形参
static T info;
//下面代码错误,不能在静态方法声明中使用类型形参
public static void bar(T msg){}
//下面代码编译时引起错误:instanceof运算符后不能使用泛型类
if ( cs instanceof List<String>){……}
上文的形参是指
正如前面讲的,使用一个泛型类时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参(继承父类泛型除外)。如果没有传入类型实参,编译器就会提出泛型警告。假设现在需要定义一个方法,该方法里有一个集合形参,集合形参的元素类型是不确定的,那应该怎么定义?
注意:如果Foo是Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G并不是G的子类型!这点非常值得注意,因为它与我们的习惯看法不同。
数组和泛型有所不同,假设Foo是Bar的一个子类型(子类或子接口),那么Foo[]依然是Bar[]的子类型;但G不是G的子类型。
如下代码是错误的:
List<Integer> iList = new ArrayList<Integer>();
//下面代码导致编译错误
List<Number> nList = iList;
为了表示各种泛型List的父类,我们需要使用类型通配符,类型通配符是一个问号,将一个问题作为类型实参传给List集合,写作:List<?>
(意思是未知类型元素的List)。这个问题被称为通配符,它的元素类型可以匹配任何类型。
public void test(List<?> c){
for (int i = 0 ; i < c.size(); i++){
System.out.println(c.get(i));
}
}
但这种带通配符的List使表示它是各种泛型List的父类,并不能把元素加入到其中。例如,如下代码将会引起编译错误。
List<?> c = new ArrayList<String>();
//下面程序引起编译错误
c.add(new Object());
因为我们不知道上面程序中c集合里元素的类型,所以不能向其中添加对象。根据前面的List接口定义的代码可以发现:add方法有类型参数E作为集合的元素类型,所以传给add的参数必须是E类的对象或者其子类的对象。但因为在该例中不知道E是什么类型,所以程序无法将任何对象“丢进”该集合。唯一的例外是null,它是所有引用类型的实例。
另一方面,程序可以调用get()方法来返回List集合指定索引处的元素,其返回值是一个未知类型,但可以肯定的是它总是一个Object。因此,把get()的返回值赋值给一个Object类型的变量,或者放在任何希望是Object类型的地方都可以。
当直接使用List这种形式时,即表明这个List集合可以是任何泛型List的父类。但还有一种特殊的情形,我们不想使这个List是所有泛型List的父类,只想表示它是某一类泛型List的父类。
//它表示所有Shape泛型List(即List后尖括号里的类型是Shape的子类)的父类
List<? extends Shape>
List是受限制通配符的例子,此处的问号代表一个未知的类型,这个未知类型一定是Shape的子类型(也可以是Shape)本身,因此我们把Shape称为这个通配符的上限(upper bound)。
因为我们不知道这个受限制的通配符的具体类型,所以不能把Shape对象或其子类的对象加入这个泛型集合中,下面的代码就是错误的
public void addRectangle(List<? extends Shaper> shapes){
//下面代码引起编译错误
shapers.add(0, new Rectangle());
}
与普通通配符类似的是,shapes.add()的第二个参数类型是? extends Shape,它表示Shape未知的子类,我们无法准确知道这个类型是什么,所以无法将任何对象添加到这种集合中。
Java泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型开通时设定上限,用于表示传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。
public class Apple<T extends Number>{
T col;
public static void main(String[] args){
Apple<Integer> ai = new Apple<>();
Apple<double> ad = new Apple<>();
//下面代码将引发编译异常,String不是Number的子类型
Apple<String> as = new Apple<>();
}
}
上面程序定义了一个Apple泛型类,该Apple类的类型形参的上限是Number类,这表明使用Apple类时为T形参传入的实际类型参数只能是Number或Number类的子类。
在一种更极端的情况下,程序需要为类型形参设定多个上限(至多有一个父类上限,可有有多个接口上限),表明该类型形参必须是其父类的子类(是父类本身也行),并且实现多个上限接口:
public class Apple<T extends Number & java.io.Serializable>{
……
}
前面介绍了在定义类、接口时可以使用类型形参,在该类的方法定义和变量定义、接口的方法定义中,这些类型形参可以被当成普通类型来用。在另外一些情况下,我们定义类、接口时没有使用类型形参,但定义方法时想自己定义类型形参,这也是可以的。Java 5 还提供了对泛型方法的支持。
所谓泛型方法(Generic Method),就是在声明方法时定义一个或多个类型的形参,格式如下:
修饰符 <T , S> 返回值类型 方法名(形参列表){…
…
}
泛型的方法签名比普通方法的方法签名多了类型形参声明,放在方法修饰符和方法返回值类型之间。例:
public class GenericMethodTest{
static <T> void fromArrayToCollection(T[] a, Collection<T> c){
for (T o : a){
c.add(o);
}
}
}
public static void main(String[] args){
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<>();
fromArrayToCollection(oa, co);//T代表Object类型
String[] sa = new String[100];
Collection<String> cs = new ArrayList<>();
fromArrayToCollection(sa, cs);//T代表String类型
fromArrayToCollection(sa, co);//T代表Object类型
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<>();
fromArrayToCollection(ia, cn);//T代表Number类型
fromArrayToCollection(fa, cn);//T代表Number类型
fromArrayToCollection(na, cn);//T代表Number类型
fromArrayTocollection(na, co);//T代表Object类型
//下面代码出现编译错误,T代表String类型,但na是一个Number数组
fromArrayToCollection(na, cs);
}
与接口、类声明中定义的类型形参不同的是,方法声明中定义的形参只能在该方法里使用。
与类、接口中使用泛型参数不同的是,方法中的泛型参数无须显式传入实际类型参数,如上面程序所示,当程序调用fromArrayToCollection()方法时,无须在调用该方法前传入String、Object等类型,但系统依然可以知道类型形参的数据类型,因为编译器根据实参推断类型形参的值,它通常推断出最直接的类型参数。
大多数时候可以用泛型方法来代替类型通配符。如:
public interface Collection<E>{
boolean containsAll(collection<?> c);
boolean addAll(Collection<? extends E> c);
……
}
上面两个方法的形参都采用了类型通配符,也可以采用泛型方法
public interface Collection<E>{
boolean <T> containsAll(collection<T> c);
boolean <T extends E> addAll(Collection<T> c);
……
}
上面方法使用了泛型形式,这时定义类型形参时设定上限。
上面两个方法中类型形参T只使用了一次,类型形参T产生的唯一效果是可以在不同的调用点传入不同的实际类型。对于这种情况,应该使用通配符:通配符就是被设计用来支持灵活的子类化的。
泛型方法允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。
如果某个方法中一个形参a的类型或返回值的类型依赖于另一个形参b的类型,则形参b的类型声明不应该使用通配符——因为形参a或返回值的类型依赖于形参b的类型,如果形参b的类型无法确定,程序就无法定义形参a的类型。在这种情况下,只能考虑使用在方法签名中声明类型形参——也就是泛型方法。
如果有需要,我们可以同时使用泛型方法和通配符,如Java的Collections.copy()方法
public class Collections{
public static <T> void copy(List<T> dest, List<? extends T> src){……}
}
把它改成 使用泛型方法,不使用类型通配符:
class Collection{
public static <T, S extends T> void copy(List<T> dest, List<S> src){……}
}
上面类型形参S仅用了一次,没有其他参数的类型、方法返回值的类型依赖于它,那类型形参S就没有存在的必要,即可以用通配符来代替S。使用通配符比使用泛型方法(在方法签名中显式声明类型形参)更加清晰和准确。
类型通配符与泛型方法还有一个显著的区别:类型通配符即可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的类型形参必须在对应方法中显式声明(即不能单独用来定义一个变量)。
正如泛型方法允许在方法签名中声明类型形参,Java也允许在构造器签名中声明类型形参,这样就产生了所谓的泛型构造器。
一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让Java根据数据参数的类型来“推断”类型形参的类型,而且程序员也可以显式地为构造器中的类型形参指定实际的类型。
class Foo{
public <T> Foo(T t){
System.out.println(t);
}
}
public class GenericConstructor{
public static void main(String[] args){
//泛型构造器中的T参数为String
new Foo("疯狂Java讲义");
//泛型构造器中的T参数为Integer
new Foo(200);
//显式指定泛型构造器中的T参数为String
new <String> Foo("疯狂Android讲义");
}
}
如果程序显式指定了泛型构造器中声明的类型形参的实际类型,则不可以使用“菱形”语法。
class MyClass<E>{
public <T> MyClass(T t){
System.out.println("t参数的值为:" + t);
}
}
public class GenericDiamondTest{
public static void main(String[] args){
//MyClass类声明中的E形参是String类型,泛型构造器中声明的T形参是Integer类型
MyClass<String> mc1 = new MyClass<>(5);
//显式指定泛型构造器中声明的T形参是Integer类型
MyClass<String> mc2 = new <Integer> MyClass<String>(5);
//MyClass类声明中的E形参是String类型如果显式指定泛型构造器中声明的T形参是Integer类型,此时就不能使用菱形语法,下面代码是错的
MyClass<String> mc3 = new <Integer> MyClass<>(5);
}
}
这个通配符表示它必须是Type本身,或是Type的父类。
public static <T> T copy(Collection<? super T> dest, Collection<T> src){
T last = null
for (T ele : src){
dest.add(ele);
}
}
泛型允许设定通配符的上、下限,从而允许在一个类里包含如下两个方法定义:
public class MyUtils{
public static <T> void copy(Collection<T> dest, Collection<? extends T> src){...}
public static <T> T copy(Collection<? super T> dest, Collection<T> src){...}
}
两个方法的参数中,前一个集合里的元素类型都是后一集合里集合元素类型的父类。这个类仅包含这两个方法不会有任何错误,但只要调用这个方法就会引起编译错误。编译器无法确定想调用哪个copy方法。
严格的泛型代码里,带泛型声明的类总应该带着类型参数,但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型参数,此时该类型参数被称作raw type (原始类型),默认是声明该参数时指定的第一个上限类型。
当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有尖括号之间的类型信息都将被扔掉。即擦除。
从逻辑上看,List是List的子类,如果直接把后者对象赋给前者的对象应该引起编译错误,实际上不会,编译器仅仅提示“未经检查的转换”
Java 5 的泛型有一个很重要的设计原则——如果一段代码在编译时没有提出“[unchecked]未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常。正是基于这个原因,数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符。但可以声明元素类型包含类型变量或类型开通的数组。
就是就说,只能声明List[]形式的数组,但是不能创建ArrayList[10]这样的数组对象。
与此类似的是,创建元素类型是类型变量的数组对象也将导致编译错误:
<T> T[] makeArray(Collection<T> coll){
//下面代码将导致编译错误
return new T[coll.size()];
}
因为类型变量在运行时并不存在,所以编译器无法确定实际类型是什么。
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对象来通知程序,从而实现将“业务功能实现代码”和“错误处理代码”分离,提供更好的可读性。
Java提出一种假设:如果程序可以顺利完成,那就“一切正常”,把系统的业务实现代码放在try块中定义,所有的异常处理逻辑放在catch块中进行处理。语法结构如下:
try{
//业务实现代码
...
}
catch (Exception e){
alert 输入不合法
goto retry
}
如果执行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及其任何子类。
public class DivTest{
public static void main(String[] args){
try{
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
int c = a / b;
System.out.println("您输入的两个数相除的结果是:" + c );
}
catch (IndexOutOfBoundsException ie){
System.out.println("数组越界:运行程序时输入的参数个数不够");
}
catch (NumberFormatException ne){
System.out.println("数字格式异常:程序只能接收整数参数");
}
catch (ArithmeticException ae){
System.out.println("算术异常");
}
catch (Exception e){
System.out.println("未知异常");
}
}
}
Java运行时的异常处理逻辑可能有如下几种情形:
- 如果运行该程序时输入的参数不够,将会发生数组越界异常,Java运行时将调用IndexOutOfBoundsException对应的catch块处理该异常
- 如果运行该程序时输入的参数不是数字,而是字母,将发生数字格式异常,Java运行时将调用NumberFormatException异常对应的catch块处理该异常
- 如果运行该程序时除数是0,将发生除0异常,Java运行时将调用ArithmeticException对应的catch块处理该异常\
- 如果程序试图调用一个null对象的实例方法或实例变量时,就会引发NullPointerException异常。
- 如果程序运行时出现其他异常,该异常对象总量Exception类或子类的实例,Java运行时将调用Exception对应的catch块处理该异常
运行异常捕获时不仅应该把Exception类对应的catch块放在最后,而且所有父类异常的catch块都应该排在子类异常catch块的后面(简称:先处理小异常,再处理大异常),否则将出现编译错误。
在Java 7 以前,每个catch块只能捕获一种类型的异常;但从Java 7 开始,一个catch块可以捕获多种类型的异常
使用一个catch块捕获多种类型的异常时需要注意如下两个地方。
- 捕获多种类型的异常时,多种异常类型之间应该用竖线(|)隔开。
- 捕获多种类型的异常时,异常变量有隐式的final修饰,因此程序不能对异常变量重新赋值
public class MultiExceptionTest{
public static void main(String[] args){
try{
int a = Integer.parseInt(args[0]);
int b = Integer.parseInt(args[1]);
int c = a / b;
System.out.println("您输入的两个数相除的结果是:" + c );
}
catch (IndexOutOfBoundsException | NumberFormatException | ArithmeticException ie){
SYstem.out.println("程序发生了数组越界、数字格式异常、算术异常之一");
//捕获多异常时,异常变量默认有final修饰,所以下面代码有错
ie = new ArithmeticException("test");
}
catch (Exception e){
System.out.println("未知异常");
//捕获一种类型的异常,异常变量没有final修饰,所以下面代码完全正确
e = new RuntimeException("test");
}
}
}
如果程序需要在catch块中访问异常对象的相关信息,则可以通过访问catch块后的异常形参来获。
所有的异常形参都包含了如下几个常用方法。
- getMessage():返回该异常的详细描述字符串
- printStackTrace():将该异常的跟踪栈信息输出到标准错误输出
- printStackTrace(PrintStream s):将该异常的跟踪栈信息输出到指定输出流
- getStackTrace():返回该异常的跟踪栈信息
try{
FileInputStream fis = new FileInputStream("a.txt");
}
catch (IOException ioe){
System.out.println(ioe.getMessage());
ioe.printStackTrace();
}
有些时候,程序在try块里打开了一些物理资源(例如数据库连接、网络连接和磁盘文件等),这些物理资源都必须显式回收。
- Java的垃圾回收机制不会回收任何物理资源,垃圾回收机制只能回收堆内存中对象所战用的内存
在哪里回收这些物理资源呢?假设程序在try块里进行资源回收,如果该try块的某条语句引起了异常,该语句后的其他语句通常不会获得执行的机会。如果在catch块里进行资源回收,但catch块完全有可能得不到执行。
为了保证一定能回收try块中打开的物理资源,异常处理机制提供了finally块。不管try块中的代码是否出现异常,也不管哪一个catch块被执行,甚至在try块或catch块中执行了return语句,finally块总会被执行。除非在try块、catch块中调用了退出虚拟机的方法。
完整的Java异常处理语法结构如下:
try{
//业务实现代码
...
}
catch (SubException e){
//异常处理块1
...
}
catch (SubException2 e){
//异常处理块2
...
}
...
finally{
//资源回收块
...
}
异常处理语法结构中只有try块是必需的,也就是说如果没有try块,则不能有后面的catch块和finally块;catch块和finally块都是可选的,但至少出现其中之一或都出现。
public class FinallyTest{
public static void main(String[] args){
FileInputStream fis = null;
try{
fis = new FileInputStream("a.txt");
}
catch (IOException ioe){
System.out.println(ioe.getMessage());
//return语句强制方法返回
return;
//使用exit退出虚拟机
//System.exit(1);
}
finally{
//关闭磁盘文件,回收资源
if (fis != null){
try{
fis.close();
}
catch (IOException ioe){
ioe.printStackTrace();
}
}
System.out.println("执行finally块里的资源回收!");
}
}
}
通常情况下,不要在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语句的功能——它允许在try关键字后紧跟一对圆括号,圆括号可以声明、初始化一个或多个资源,此处的资源是指那些必须在程序结束时显式关闭的资源(比如数据库连接、网络连接等),try语句在该语句结束时自动关闭这些资源。
为了保证try语句可以正常关闭资源,这些资源实现类必须实现AutoCloseable或Closeable接口,实现这两个接口就必须实现colse()方法。
Closeable是AutoCloseable的子接口,可以被自动关闭的资源类要么实现AutoCloseable接口,要么实现Closeable接口。Closeable接口里的close()方法声明抛出了IOException,因此它的实现类在实现close()方法时只能声明抛出IOException或其子类;AutoCloseable接口里的close()方法声明抛出了Exception,因此它的实现在实现close()方法时可以声明抛出任何异常。
public class AutoCloseTest{
public static void main(String[] args) throws IOException{
try (
//声明、初始化两个可关闭的资源,try语句会自动关闭这两个资源
BufferedReader br = new BufferedReader(new FileReader("AutoCloseTest.java"));
PrintStream ps = new PrintStream(new FileOutputStream("a.txt")))
{
//使用两个资源
System.out.println(br.readLine());
ps.println("庄生晓梦迷蝴蝶");
}
}
}
上面程序中粗体字代码分别声明、初始化了两个IO流,由于BufferedReader、PrintStream都实现了Closeable接口,而且它们放在try语句中声明、初始化,所以try语句会自动关闭它们。因此上面程序是安全的。
自动关闭资源的语句相当于包含了隐式的finally块(这个finally块用于关闭资源),因此这个try语句可以既没有catch块,也没有finally块。
Java 7 几乎把所有的“资源类”(包括文件IO的各种类、JDBC编程的Connection、Statement等接口……)进行了改写,改写后资源类都实现了AutoCloseable或Closeable接口。
Java的异常分为两大类:Checked异常和Runtime异常(运行时异常)。所有的RuntimeException类及其子类的实例被称为Runtime异常;不是RuntimeException类及其子类的异常实例则被称为Checked异常。
只有Java语言提供了Checked异常,其他语言都没有。Java程序必须显式处理Checked异常,如果程序没有处理该异常,则无法通过编译。
对Checked异常的处理方式有如下两种:
- 当前方法明确知道如何处理该异常,程序应该使用try...catch块来捕获该异常不,然后在对应的catch块中修复该异常。
- 当前方法不知道如何处理这种异常,应该在定义该方法时声明抛出该异常
Runtime异常则更灵活,Runtime异常无须显式声明抛出,如果程序需要捕获Runtime异常,也可以使用try...catch块来实现
大部分的方法总是不能明确地知道如何处理异常,因此只能声明抛出该异常,这种情况如此普遍,所以Checked异常降低了程序开发的生产率和代码的执行效率。
使用throws声明抛出异常的思路是:当前方法不知道如何处理这种类型的异常,该异常应该由上一级调用者处理;如果main方法也不知道如何处理这种类型的异常,也可以使用throws声明抛出异常,该异常将交给JVM处理。JVM对异常的处理方法是,打印异常的跟踪栈信息,并中止程序运行。
throws声明抛出只能在方法签名中使用,throws可以声明抛出多个异常类,多个异常类之间用逗号隔开。语法格式如下:
throws ExceptionClass1, ExceptionClass2...
下面程序使用了throws来声明抛出IOException异常,一旦使用throws语句声明抛出该异常,程序就无须使用try...catch块来捕获该异常了
public static void main(String[] args) throws IOException{
FileInputStream fis = new FIleInputStream("a.txt");
}
上面程序声明不处理IOException异常,将该异常交给JVM处理,所以程序一旦遇到该异常,JVM就会打印该异常的跟踪栈信息,并结束程序。
如果某段代码中调用了一个带throws声明的方法,该方法声明抛出了Checked异常,则表明该方法希望它的调用者来处理该异常。也就是说,调用该方法时要么放在try块中显式捕获该异常,要么放在另一个带throws声明抛出的方法中。
public class ThrowsTest{
public static void main(String[] args) throws Exception{
//因为test()方法声明抛出IOException异常,所以调用该方法的代码要么处于try...catch块中,要么处于另一个带throws声明抛出的方法中
test();
}
public static void test() throws IOException{
//因为FileInputStream的构造器声明抛出IOException异常,所以调用 FileInputStream的代码要么处于try...catch块中,要么处于另一个带throws声明抛出的方法中
FileInputStream fis = new FileInputStream("a.txt");
}
}
使用throws声明抛出异常时有一个限制,就是方法重写时“两小”中的一条规则:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
使用Checked异常至少有两大不便之处:
- 对于程序中的Checked异常,Java要求必须显式捕获并处理该异常,或者显式声明抛出该异常,这样就增加了编程复杂度
- 如果在方法中显式声明抛出Checked异常,将会方法签名与异常耦合,如果该方法是重写父类的方法,则该方法抛出的异常还会受到被重写方法所抛出异常的限制。
使用Runtime异常时,程序无须在方法中声明抛出Checked异常,一旦发生了自定义错误,程序只管抛出Runtime异常即可。如果程序需要在合适的地方捕获异常并对异常进行处理,则一样可以在try...catch块来捕获Runtime异常。
当程序出现错误时,系统会自动抛出异常;除此之外,Java也允许程序自行抛出异常,自行抛出异常使用throw语句来完成(注意此处throw后面没有s,与前面声明抛出的throws是有区别的)。
如果程序中的数据、执行与既定的业务不符,这就是一种异常,必须由程序员来决定抛出,系统无法抛出这种异常。
如果需要在程序中自行抛出异常,则应使用throw语句,throw语句可以单独使用,throw语句抛出的不是异常类,而是一种异常实例,而且每次只能抛出一个异常实例。语法格式如下:
throw ExceptionInstance;
不管系统自动抛出的异常,还是程序员手动抛出的异常,Java运行环境对异常的处理没有任何差别。
如果throw语句抛出的异常是Checked异常,则该throw 语句要么处于try块里,显式捕获该异常,要么放在一个带throws声明抛出的方法中,即把该异常交给方法的调用者处理;如果throw语句抛出的异常是Runtime异常,则该语句无须放在try块里,也无须放在带throw声明抛出的方法中;程序既可以显式使用try...catch来捕获并处理该异常,也可以完全不理会该异常,把该异常交给该方法调用者处理。
public class ThrowTest{
public static void main(String[] args){
try{
//调用声明抛出Checked异常的方法,要么显式捕获该异常,要么在main方法中再次声明抛出
throwChecked(-3);
}
catch (Exception e){
System.out.println(e.getMessage());
}
//调用声明抛出Runtime异常的方法既可以显式捕获该异常,也可不理会该异常
throwRuntime(3);
}
public static void throwChecked(int a) throws Exception{
if (a > 0){
//自行抛出Exception异常,该代码必须处于try块里,或处于带throws声明的方法中
throw new Exception("a的值大于0,不符合要求");
}
}
public static void throwRuntime(int a){
if (a > 0){
//自行抛出RuntimeException异常,既可以显式捕获该异常,也可完全不理会该异常,把该异常交给该方法调用者处理
throw new RuntimeException("a的值大于0,不符合要求");
}
}
}
在通常情况下,程序很少会自行抛出系统异常,因为异常的类名通常也包含了该异常的有用令牌,所以在选择抛出异常时,应该选择合适的异常类,从而可以明确地描述该异常情况。在这种情形下,应用程序常常需要抛出自定义异常。
用户自定义异常都应该继承Exception基类,如果希望自定义Runtime异常异常,则应该继承RuntimeException基类。定义异常类时通常需要提供两个构造器:一个是无参数的构造器;另一个是带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(也就是异常对象的getMessage()方法的返回值)。
public class AuctionException extends Exception{
//无参数的构造器
public AuctionException(){}
//带一个字符串参数的构造器
public AuctionException(String msg){
super(msg);
}
}
上面程序super调用可以将此字符串参数传给异常对象的message属性,该message属性就是该异常对象的详细描述信息。
如果需要自定义Runtime异常,只需将上面程序中的Exception基类改为RuntimeException基类,其他地方无须修改。
大部分情况下,创建自定义异常都可以采用与上方相似的代码完成,只需改变AuctionException异常的类名即可,让该异常类的类名可以准确描述该异常。
前面介绍的异常处理方式有如下两种:
- 在出现异常的方法内捕获并处理异常,该方法的调用者将不能再次捕获该异常
- 该方法签名中声明抛出该异常,将该异常完全交给方法调用者处理。
在实际应用中往往需要更复杂的处理方式——当一个异常出现时,单靠某个方法无法完全处理该异常,必须由几个方法协作才可完全处理该异常。也就是说,在异常出现的当前方法中,程序只对异常进行部分处理,还有些处理需要在该方法的调用者中才能完成,所以应该再次抛出异常,让该方法的调用者也能捕获到异常。
为了实现这种通过多个方法协作处理同一个异常的情形,可以在catch块中结合throw语句来完成。
public class AuctionTest{
private double initPrice = 30.0;
//因为该方法中显式抛出了AuctionException异常
//所以此处需要声明抛出AuctionException异常
//AuctionException即上文的代码
public void bid(String bidPrice) throws AuctionException{
double d = 0.0;
try{
d = Double.parseDouble(bidPrice);
}
catch (Exception a){
//此处完成本方法中可以对异常执行的修复处理
//此处仅仅是在控制台打印异常的跟踪栈信息
e.printStackTraceI();
//再次抛出自定义异常
throw new AuctionException("竞拍价必须是数值,不能包含其他字符!");
}
if (initPrice > d) {
throw new AuctionException("竞拍价比起拍价低,不允许竞拍!");
}
initPrice = d;
}
public static void main(String[] args){
AuctionTest at = new AuctionTest();
try{
at.bid("df");
}
catch (AuctionException ae){
//再次捕获到bid()方法中的异常,并对该异常进行处理
//将该异常的详细描述信息输出到标准错误输出
System.err.println(ae.getMessage());
}
}
}
这种catch和throw结合使用的情况在大型企业级应用中非常常用。企业级应用对异常的处理通常分成两个部分:
- 应用后台需要通过日志来记录异常发生的详细情况
- 应用还需要根据异常向应用使用者传达某种提示。
在这种情形下,所有异常都需要两个方法共同完成,也就必须将catch和throw结合使用。
对于如下代码
try {
new FileOutputStream("a.txt");
}
catch (Exception ex){
ex.printStackTrace();
throw ex;
}
上面代码再次抛出了捕获到的异常,程序捕获该异常时,声明该异常的类型为Exception;但实际上try块中可能只调用了FileOutputStream构造器,这个构造器声明只是抛出了FileNotFoundException异常。
在Java 7 以前,由于在捕获该异常时声明ex的类型是Exception,因此Java编译器认为这段代码可能抛出Exception异常,所以包含这段代码的方法通常需要声明抛出Exception异常。
从Java 7 开始,Java编译器会检查throw语句抛出异常的实际类型,这样编译器知道上述代码实际上只可能抛出FileNotFoundException异常,因此在方法签名中只要声明抛出FileNotFoundException异常即可
对于真实的企业级应用而言,常常有严格的分层关系,上层功能的实现严格依赖于下层的API,也不会跨层访问。
表现层:用户界面----->中间层:实现业务逻辑----->持久层:保存数据
对于采用上图结构的应用,当业务逻辑层访问持久层出现SQLException异常时,程序不应该把底层的SQLException异常传到用户界面,有如下两个原因:、
- 对于正常用户而言,他们不想看到底层SQLException异常,SQLException异常对他们使用该系统没有任何帮助
- 对于恶意用户而言,将SQLException异常暴露出来不安全
把底层的原始异常直接传给用户是一种不负责任的表现。通常的做法是:程序先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息,这各处理方式被称为异常转译。
假设程序需要实现工资计算的方法,则程序应该采用如下结构的代码来实现该方法:
public calSal() throws SalException{
try{
//实现结算工资的业务逻辑
...
}
catch (SQLException sqle){
//把原始异常记录下来,留给管理员
...
//下面异常中的message就是对用户的提示
throw new SalException("访问底层数据库出现异常");
}
catch (Exception e){
//把原始异常记录下来,留给管理员
...
//下面异常中的message就是对用户的提示
throw new SalException("系统出现异常");
}
}
这种把原始异常信息隐藏起来,仅向上提供必要的异常提示信息的处理方式,可以保证底层异常不会扩散到表现层,可以避免向上暴露太多的实现细节,这完全符合面向对象的封装原则。
这种把捕获一个异常然后接着抛出另一个异常,并把原始异常信息保存下来是一种典型的链式处理(23种设计模式之一:职责链模式),也被称为“异常链”。
在JDK 1.4 以前,程序员必须自己编写代码来保持原始异常信息。从JDK 1.4 以后,所有Throwable的子类在构造器中都可以接收一个cause对象作为参数。这个cause就用来表示原始异常,这样可以把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,你也能通过这个异常链追踪到异常最初发生的位置。如果我们希望上面的SalException可以追踪到最原始的异常信息,则可以改写成如下形式:
public calSal() throws SalException{
try{
//实现结算工资的业务逻辑
...
}
catch (SQLException sqle){
//把原始异常记录下来,留给管理员
...
//下面异常中的sqle就是原始异常
throw new SalException(sqle);
}
catch(Exception e){
//把原始异常记录下来,留给管理员
...
//下面异常中的e就是原始异常
throw new SalException(e);
}
}
上面代码创建SalException对象时,传入了一个Exception对象,而不是传入一个String对象,这就需要SalException类有相应的构造器。从JDK 1.4 以后,Throwable基类已经有了一个可以接收Exception方法,所以可以采用如下代码来定义SalException类。
public class SalException extends Exception{
public SalException(){}
public SalException(String msg){
super(msg);
}
public SalException(Throwable t){
super(t);
}
}
异常对象的printStackTrace()方法用于打印异常的跟踪栈信息,根据printStackTrace()方法的输出结果,我们可以找到异常的源头,并跟踪到异常一路触发的过程。
在面向对象的编程中,大多数复杂的操作都会被分解成一系列方法的调用。所以面向对象的应用程序运行时,经常会发生一系列方法调用,从而形成“方法调用栈”,异常的传播则相反:只要异常没有被完全捕获(包括异常没有被捕获,或异常被处理后重新抛出了新异常),异常从发生异常的方法逐渐向外传播,首先传给该方法的调用者,该方法调用者再次传给其调用者……直到最后传到main方法,如果main方法依然没有处理该异常,JVM会中止该程序,并打印异常的跟踪栈信息。
跟踪栈总是最内部的被调用方法逐渐上传,直到最外部业务操作的起点,通常就是程序的入口main方法或Thread类的run方法(多线程的情形)。
虽然printStackTrace()方法可以很方便地用于追踪异常的发生情况,可以用它来高度程序,但在最后发布的程序中,应该避免使用它;而应该对捕获的异常进行适当的处理,而不是简单地将异常的跟踪栈信息打印出来。
成功的异常处理应该实现如下4个目标:
- 使程序代码混乱最小化
- 捕获并保留诊断信息
- 通知合适的人员
- 适用合适的方式结束异常活动
滥用异常机制会带来一些负面影响。过度使用异常主要有两个方面:
- 把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地抛出异常来代替所有的错误处理
- 使用异常处理来代替流程控制
熟悉了异常使用方法后,程序员可能不再愿意编写烦琐的错误处理代码,而是简单地抛出异常。实际上这样做是不对的,对于完全已知的错误,应该编写处理这种错误的代码,增加程序的健壮性;对于普通的错误,应该编写处理这种错误的代码,增加程序的健壮性。只有对外部的、不能确定和预知的运行时错误才使用异常。
异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分享,因此绝不要使用异常处理来代替正常的业务逻辑判断。
另外,异常机制的效率比正常的流程控制效率差,所以不要使用异常处理来代替正常的流程控制。异常只应该用于处理非正常的情况,对于一些完全可预知,而且处理方式清楚的错误,程序应该提供相应的错误处理代码,而不是将其笼统地称为异常。
try块里的代码过于庞大,业务过于复杂,就会造成try块中出现异常的可能性大大增加,从而导致分析异常原因的难度也大大增加。而且当try块过于庞大时,就在try块后紧跟大量的catch块才可以针对不同的异常提供不同的处理逻辑。同一个try块后紧跟大量的catch则需要分析它们之间的逻辑关系,反而增加了编程复杂度。
正确的做法是,把大块的try块分割成多个可能出现异常的程序段落,并把它们放在单独的try块中,从而分别捕获并处理异常。
所谓Catch All语句指的是一种异常捕获模块,它可以处理程序所有可能异常。
try{
//可能引发Checked异常的代码
}
catch (Throwable t){
//进行异常处理
t.printStackTrace();
}
编写关键程序时应该避免使用这种异常处理方式,它有两点不足之处:
- 所有的异常都采用相同的处理方式,这将导致无法对不同的异常分情况处理,如果要分情况处理,则需要在catch块中使用分支语句进行控制,这是得不偿失的做法。
- 这种捕获方式可能将程序中的错误、Runtime异常等可能导致程序终止的情况全部捕获到,从而“压制”了异常。如果出现了一些“关键”异常,那么此异常也会被“静悄悄”地忽略。
Catch All语句不过是一种通过避免错误处理而加快编程进度的机制,应尽量避免在实际应用中使用这种语句
不要忽略异常!既然已捕获到异常,那catch块理应做些有用的事情——处理并修复这个错误。catch块整个为空,或者仅仅打印出错信息都是不妥的!
通常建议对异常采取适当措施,比如:
- 处理异常。对异常进行合适的修复,然后绕过异常发生的地方继续执行;或用别的数据进行计算,以代替期望的方法返回值;或者提示用户重新操作……总之,对于Checked异常,程序应该尽量修复
- 重新抛出新异常。把当前运行环境下能做的事情尽量做完,然后进行异常转译,把异常包装成当前层的异常,重新抛出给上层调用者。
- 在合适的层处理异常。如果当前层不清楚如何处理异常,就不要在当前层使用catch语句来捕获该异常,直接使用throws声明抛出该异常,让上层调用者来负责处理该异常。
Java使用AWT和Swing类完成图形用户界面编程,其中AWT的全称是抽象窗口工具集(Abstract Window Toolkit),它是Sun最早提供的GUI库,这个GUI库提供了一些基本功能,但这个GUI库的功能比较有限,所以后来又提供了Swing库。通过使用AWT和Swing提供的图形界面组件库,Java的图形用户界面编程非常简单,程序只要依次创建所需的图形组件,并以合适的方式将这些组件组织在一起,就可以开发出非常美观的用户界面。
程序以一种“搭积木”的方式将这些图形用户组件组织在一起,就是实际可用的图形用户界面,但这些图形用户界面还不能与用户交互,为了实现图形用户界面与用户交互操作,还应为程序提供事件处理,事件处理负责让程序可以响应用户的动作。
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则是容器管理其他组件而已的方式
任何窗口都可被分解成一个空的容器,容器里盛装了大量的基本组件,通过设置这些基本组件的大小、位置等属性,就可以将该空的容器和基本组件组成一个整体的窗口。
容器(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创建了一个窗口。
import java.awt.Frame;
public class FrameTest{
public static void main(String[] args){
Frame f = new Frame("测试窗口");
f.setBounds(30, 30, 250, 200);//设置窗口的大小、位置
f.setVisible(true);//将窗口显示出来(Frame对象默认处于隐藏状态)
}
}
运行上面程序出现的窗口,如果单击右上角的X,该窗口不会关闭,这是因为我们还未为该窗口编写任何事件响应。
Panel(面板)是AWT中另一个典型的容器,它代表不能独立存在、必须放在其他容器中的容器。Panel外在表现为一个矩形区域,该区域内可盛装其他组件。Panel容器存在的意义在于为其他组件提供空间,Panel容器具有如下几个特点:
- 可作为容器来盛装其他组件,为放置组件提供空间
- 不能单独存在,必须放置到其他容器中
- 默认使用FlowLayout作为其布局管理器
下面使用Panel作为容器来盛装一个文本框和一个按钮,并将该Panel对象添加到Frame对象中:
import java.awt.*;
public class PanelTest{
public static void main(String[] args){
Frame f = new Frame("测试窗口");
Panel p = new Panel();
p.add(new TextField(20));
p.add(new Button("单击我"));
f.add(p);//将Panel容器添加到Frame窗口中
f.setBounds(30, 30, 250, 120);
f.setVisible(true);
}
}
ScrollPane是一个带滚动条的窗口,它也不能单独存在,必须被添加到其他容器中。ScrollPane有如下几个特点:
- 可作为容器来盛装其他组件,当组件占用空间过大时,ScrollPane自动产生滚动条。当然也可以通过指定特定的构造器参数来指定默认具有滚动条
- 不能单独存在,必须放置到其他容器中
- 默认使用BorderLayout作为其布局管理器。ScrollPane通常用于盛装其他容器,所以通常不允许改变ScrollPane的布局管理器
下面使用ScrollPane容器来代替Panel容器
import java.awt.*;
public class ScrollPaneTest{
public static void main(String[] args){
Frame f = new Frame("测试窗口");
ScrollPane sp = new ScrollPane(ScrollPane.SCROLLBARS_ALWAYS);//创建ScrollPane并指定总是具有滚动条
sp.add(new TextField(20));
sp.add(new Button("单击我"));
f.add(sp);//将ScrollPane容器添加到Frame对象中
f.setBounds(30, 30, 250, 120);
f.setVisible(true);
}
}
上面程序虽然向ScrollPane容器添加了一个文本框和一个按钮,但只能看到一个按钮,却看不到文本框。这是因为ScrollPane使用BorderLayout布局管理器的缘故,而BorderLayout导致了该容器中只有一个组件被显示出来。
Container是Componenet的子类。
Window、Panel和ScrollPane是Container的子类
Frame和Dialog是Window的子类。Applet是Panel的子类
为了使生成的图形用户界面具有良好的平台无关性,Java语言提供了布局管理器这个工具来管理组件在容器中的布局而不使用直接设置组件位置和大小的方式。
组件在不同的平台上大小可能不一样,Java提供了LayoutManager,它根据运行平台来调整组件的大小,程序员只须为容器选择合适的布局管理器。
所有的AWT容器都有默认的布局管理器,如果没有为容器指定布局管理器,则该容器使用默认的布局管理器。为容器指定布局管理器通过调用容器对象的setLayout(LayoutManager lm)方法来完成。如下代码所示:
c.setLayout(new XxxLayout());
AWT提供了FlowLayout、BorderLayout、GridLayout、GridBagLayout、CardLayout5个常用的布局管理器。Swing还提供了一个BoxLayout布局管理器。
在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)。
import java.awt.Frame;
Frame f = new Frame("测试窗口");
//设置Frame容器使用FlowLayout布局管理器
f.setLayout(new FlowLayout(FlowLayout.LEFT, 20, 5));
//向容器添加10个按钮
for (int i = 0; i < 10; i++){
f.add(new Button("按钮" + i));
}
//设置容器为最佳大小
f.pack();
f.setVisible(true);
pack()方法 是Window容器提供的一个方法,该方法用于将容器调整到最佳大小。通过Java编写图形用户界面程序时,很少直接设置窗口的大小,通常用pack()方法调整。
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。
Frame f = new Frame("测试窗口");
//设置Frame容器使用BorderLayout布局管理器
f.setLayout(new BorderLayout(30, 5));
f.add(new Button("南"), SOUTH);
...
//默认添加到中间区域中
f.add(new Button("中"));
...
//设置窗口为最佳大小
f.pack();
f.setVisible(true);
BorderLayout最多只能放置5个组件,但可以放置少于5个组件。如果某区域没有放置组件,该区域不会出现空白,旁边区域的组件会自动占据该区域。
虽然BorderLayout最多只能放置5个组件,但因为容器也是一个组件,所以我们可以先向Panel里添加多个组件,再把Panel添加到BorderLayout布局管理器中,从而让BorderLayout布局管理中的实际组件数远远超出5个。
Frame f = new Frame("测试窗口");
f.setLayout(new BorderLayout(30,5));
f.add(new Button("南"), BorderLayout.SOUTH);
f.add(new Button("北"), BorderLayout.NORTH);
//创建一个Panel对象
Panel p = new Panel();
//向Panel对象中添加两个组件
p.add(new TextField(20));
p.add(new Button("单击我"));
//默认添加到中间区域中,向中间区域添加一个Panel容器
f.add(p);
f.add(new Button("东"), BorderLayout.EAST);
f.pack();
f.setVisible(true);
GridLayout将容器分割成纵横线分隔的网格,每个风格所占的区域大小相同。当向使用GridLayout布局管理器的容器中添加组件时,默认从左向右、从上向下依次添加到每个风格中。
与FlowLayout不同的是,放置在GridLayout中的各组件的大小由组件所处的区域来决定(每个组件将自动占满整个区域)
GridLayout有如下两个构造器:
- GridLayout(int rows, int cols):采用指定的行数、列数,以及默认的横向间距、纵向间距将容器分割成多个网格
- GridLayout(int rows, int cols, int hgap, int vgap):采用指定的行数、列数,以及指定的横向间距、纵向间距将容器分割成多个网格。
如下程序结合BorderLayout和GridLayout开发了一个计算器的可视化窗口
Frame f = new Frame("计算器");
Panel p1 = new Panel();
p1.add(new TextField(30));
f.add(p1, BorderLayout.NORTH);
Panel p2 = new Panel();
//设置Panel使用GridLayout布局管理器
p2.setLayout(new GridLayout(3,5,4,4));
String[] name = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "-", "*", "/", ".");
//向Panel中依次添加15个按钮
for (int i = 0; i < name.length; i++){
p2.add(new Button(name[i]));
}
//默认将Panel对象添加到Frame窗口的中间
f.add(p2);
f.pack();
f.setVisible(true);
GridBagLayout功能最强大也最复杂,与GridLayout不同的是在GridBagLayout中,一个组件可以跨越一个或多个网格,并可以设置各风格的大小互不相同,从而增加了布局的灵活性。当窗口大小发生变化时,GridBagLayout也可以准确地控制窗口各部分的拉伸
为了处理GridBagLayout中GUI组件的大小、跨越性,Java提供了GridBagConstraints对象,该对象与特定的GUI组件关联,用于控制该GUI组件的大小、跨越性。
使用GridBagLayout的步骤如下:
<1>创建GridBagLayout,并指定GUI容器使用该布局管理器
GridBagLayout gb = new GridBagLayout();
container.setLayout(gb);
<2>创建GridBagConstraints对象,并设置该对象相关属性(用于设置受该对象控制的GUI组件的大小、跨越性等)。
gbc.gridx = 2;//设置受该对象控制的GUI组件位于网格的横向索引
gbc.gridy = 1;//位于网格的纵向索引
gbc.gridwidth = 2;//横向跨越2格
gbc.gridheight = 1;//纵向跨越1格
<3>调用GridBagLayout对象的方法来建立GridBagConstraints对象和受控制组件之间的关联
gb.setConstraints(c,gbc);//设置c组件受gbc对象控制
<4>添加组件,与采用普通管理器添加组件的方法完全一样
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属性。
public class Test{
private Frame f = new Frame("测试窗口");
private GridBagLayout gb = new GridBagLayout();
private GridBagConstraints gbc = new GridBagConstraints();
private Button[] bs = new Button[10];
//初始化
public void init(){
f.setLayout(gb);
//创建十个按钮
for(int i = 0; i < bs.length ; i++){
bs[i] = new Button("按钮" + i);
}
//所有组件都可以在横向、纵向上扩大
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1;
addButton(bs[0]);
addButton(bs[1]);
addButton(bs[2]);
//横向最后一个组件
gbc.gridwidth = GridBagConstraints.REMAINDER;
addButton(bs[3]);
//横向不会扩大
gbc.weightx = 0;
addButton(bs[4]);
//横跨两个网格
gbc.gridwidth = 2;
addButton(bs[5]);
//横跨1个网格
gbc.gridwidth = 1;
//纵跨2个网格
gbc.gridheight = 2;
//横向最后一个组件
gbc.gridwidth = GridBagConstraints.REMAINDER;
addButton(bs[6]);
//横跨1格,纵跨2格
gbc.gridwidth = 1;
gbc.gridheight = 2;
//纵向扩大权重为1
gbc.weighty = 1;
addButton(bs[7]);
//纵向不会扩大
gbc.weighty = 0;
//横向最后一个组件
gbc.gridwidth = GridBagConstraints.REMAINDER;
//纵跨1格
gbc.gridheight = 1;
addButton(bs[8]);
addButton(bs[9]);
f.pack();
f.setVisible(true);
}
private void addButton(Button button){
gb.setConstraints(button, gbc);//button受gbc控制
f.add(button);
}
public static void main(String[] args){
new Test().init();
}
}
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):显示指定名字的卡片
import java.awt.*;
import java.awt.event.*;
public class Test{
Frame f = new Frame("测试窗口");
String[] names = {"第一张", "第二张", "第三张", "第四张", "第五张"};
Panel pl = new Panel();
public void init(){
final CardLayout c = new CardLayout();
pl.setLayout(c);
for(int i = 0; i < names.length; i++){
pl.add(names[i], new Button(names[i]));
}
Panel p = new Panel();
//控制显示上一张的按钮
Button previous = new Button("上一张");
previous.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
c.previous(pl);
}
});
//控制显示下一张的按钮
Button next = new Button("下一张");
next.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
c.next(pl);
}
});
//控制显示第一张的按钮
Button first = new Button("第一张");
first.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
c.first(pl);
}
});
//控制显示最后一张的按钮
Button last = new Button("最后一张");
last.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
c.last(pl);
}
});
//控制根据Card名显示的按钮
Button third = new Button("第三张");
third.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
c.show(pl, "第三张");
}
});
p.add(previous);
p.add(next);
p.add(first);
p.add(last);
p.add(third);
f.add(pl);
f.add(p, BorderLayout.SOUTH);
f.pack();
f.setVisible(true);
}
public static void main(String[] args){
new Test().init();
}
}
Java容器中采用绝对定位的步骤如下
- 将Container的布局管理器设成null: setLayout(null)
- 向容器中添加组件时,先调用setBounds()或setSize()方法来设置组件的大小、位置。或者直接创建GUI组件时通过构造参数指定该组件的大小、位置,然后将该组件添加到容器中。
Frame f = new Frame("测试窗口");
Button b1 = new Button("第一个按钮");
Button b2 = new Button("第二个按钮");
f.setLayout(null);
b1.setBounds(20, 30, 90, 28);
f.add(b1);
b2.setBounds(50, 45, 120, 35);
f.add(b2);
f.setBounds(50, 50, 200, 100);
f.setVisible(true);
绝对定位更灵活,但使GUI界面失去跨平台特性。
GridBagLayout虽然功能强大但太复杂了,所以Swing引入了一个新的布局管理器BoxLayout,它保留了GridBagLayout的很多优点,但却没那么复杂。BoxLayout可以在垂直和水平两个方向上摆放GUI组件。
BoxLayout提供了如下一个简单的构造器
- BoxLayout(Container target, int axis):指定创建基于target容器的BoxLayout布局管理器,里面的组件安axis方向排列。其中Axis有BoxLayout.X_AXIS(横向)和BoxLayout.Y_AXIS(纵向)两个方向
Frame f = new Frame("Test");
f.setLayout(new BoxLayout(f, BoxLayout.Y_AXIS));
f.add(new Button("Fist Button");
...
BoxLayout通常和Box窗口结合使用,Box是一个特殊的容器,它有点像Panel,但Box默认使用BoxLayout布局管理器,Panel默认用FlowLayout。
Box提供了如下两个静态方法来创建Box对象:
- createHorizontalBox():创建一个水平排列组件的Box容器
- createVerticalBox():创建垂直排列组件的Box
一旦获得了Box容器后,就可以使用Box来盛装普通的GUI组件,然后将这些Box组件添加到其他容器中。从而形成整体的窗口布局。
Frame f = new Frame("Test");
Box horizontal = Box.createHorizontalBox();
Box vertical = Box.createVerticalBox();
public void init(){
horizontal.add(new Button("水平按钮一"));
horizontal.add(new Button("水平按钮二"));
vertical.add(new Button("垂直按钮一"));
vertical.add(new Button("垂直按钮二"));
f.add(horizontal, BorderLayout.NORTH);
f.add(vertical);
f.pack();
f.setVisible(true);
}
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只提供了一些常用的GUI组件。
- Button:按钮,可接受单击操作
- Canvas:用于绘图的画布
- Checkbox:复选框组件(也可变成单选框组件)
- CheckboxGroup:用于将多个CheckBox组件合成一组,一组Checkbox组件将只有一个可以被选中,即全部变成单选框组件
- Choice:下拉式选择框组件
- Frame:窗口
- Label:标签,用于放置提示性文件
- List:列表框,可以添加多项条目
- Panel:不能单独存在的基本容器类,必须放到其他窗口中。
- Scrollbar:滑动条组件。如果需要用户输入位于某个范围的值,就可以使用滑动条组件,比如调色板中设置RGB的3个值所用的滑动条。当创建一个滑动条时,必须指定它的方向、初始值、滑块的大小、最小值和最大值。
- ScrollPane:带水平及垂直滚动条的容器组件
- TextArea:多行文本域
- TextField:单行文本框
用法详见API。掌握它们的用法之后,就可以借助IDE工具来设计GUI界面,使用IDE工具可以更快地设计出更美观的GUI界面。
Dialog是Window类的子类,是一个容器类,属于特殊组件。对话框是可以独立存在的顶级窗口,但它通常依赖于其他窗口(parent窗口),对方框有模式和非模式两种。模式对话框被关闭之前,它依赖的窗口无法获得焦点。
Dialog的构造器可能有如下3个参数:
- owner:指定该对话框所依赖的窗口,既可以是窗口,也可以是对话框。
- title:标题
- modal:是否模式
Dialog d1 = new Dialog(f, "模式对话框", true);
Dialog类还有一个子类:FileDialog,它代表一个文件对话框。
构造器支持parent、title和mode三个构造参数。mode指定该窗口是用于打开或保存文件,对应参数值为:FileDialog.LOAD、FileDialog.SAVE
FileDialog的modal取决于运行平台的实现。
FileDialog提供了如下两个方法来获取被打开/保存文件的绝对路径
- getDirectory():绝对路径
- getFile():文件名
- 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
Frame f = new Frame("Test");
//关闭窗口
f.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e){
System.exit(0);
}
});
实现事件监听器对象有如下几种形式:
- 内部类形式
- 外部类形式
- 类本身作为事件监听器类
- 匿名内部内形式
示例见上文
比较少见,主要有两原因:
- 事件监听器通常属于特定的GUI,定义成外部类不利于提高程序的内聚性
- 外部类形式的事件监听器不能自由访问创建GUI界面类中的组件,编程不够简洁
如果某事件监听器需要被多个GUI界面共享,而且主要完成某种业务逻辑的实现,则可以用外部类形式。
一般不推荐将业务逻辑实现写在事件监听器中,这会导致程序的显示逻辑和业务逻辑耦合,从而拉架程序后期的维护难度。可以考虑使用业务逻辑组件来定义业务逻辑功能,再让事件监听器来调用业务逻辑组件的方法。
GUI界面类直接作为监听器类,是早期AWT事件编程常用形式,有两个缺点。
- 混乱的程序结构。GUI界面职责主要是完成界面初始化工作,但此时还需包含事件处理方法,降低了程序的可读性。
- 如果GUI界面类需要继承事件适配器,将导致该GUI界面类不能继承其他父类。
大部分时候事件处理器没有复用价值。此方式应用最广泛。
AWT中的菜单由如下几个类组合而成
- MenuBar:菜单条。菜单的容器。
- Menu:菜单组件。菜单项的容器,它也是MenuItem的子类,可作为菜单项使用
- PopupMenu:上下文菜单组件(右键菜单组件)
- MenuItem:菜单项组件。
- CheckboxMenuItem:复选框菜单项组件。
- MenuShortcut:菜单快捷键组件
MenuBar可用于盛装Menu,Menu可用于盛装MenuItem(包括Menu)。PopupMenu无须使用MenuBar盛装。
Menu、MenuItem的构造器都可以接收一个字条串参数,作为标签文本。
MenuItem还可以接收一个MenuShortcut对象,用于指定快捷键。使用虚拟代码而不是字符。如Ctrl+A:
MenuShortcut ms = new MenuShortcut(KeyEvent.VK_A);
//如果还需要Shift
MenuShortcut ms = new MenuShortcut(KeyEvent.VK_A, true);
//创建exitItem菜单项,指定用Ctrl+X快捷键
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()方法为该窗口添加菜单条。
Frame f = new Frame("Test");
MenuBar mb = new MenuBar();
//为f窗口设置菜单条
f.setMenuBar(mb);
AWT的菜单组件不能创建图标菜单,如需图标则应使用Swing的菜单组件:JMenuBar、JMenu、JmenuItem和JPopupMenu组件。
即PopupMenu对象。步骤如下:
- 创建PopupMenu实例
- 创建多个MenuItem实例,依次将这些实例加入PopupMenu中
- 将PopupMenu加入到目标组件中
- 为需要出现上下文菜单的组件编写鼠标监听器,当用户释放鼠标右键时弹出右键菜单
PopupMenu pop = new PopupMenu();
Panel p = new Panel();
p.add(pop);
//添加鼠标事件监听器
p.addMouseListener(new MouseAdapter(){
public void mouseReleased(MouseEvent e){
//如果释放的是鼠标右键
if ( e.isPopupTrigger()){
pop.show(p, e.getX(), e.getY());
}
}
});
AWT并没有为GUI组件提供实现,它仅仅是调用运行平台的GUI组件来创建和平台一致的对等体。因此程序中的TextArea实际上是Windows的多行文本域组件的对等体,具有和它相同的行为,所以TextArea默认就具有右键菜单。
Component类提供了和绘图有关的三个方法:
- paint(Graphics g):绘制组件的外观
- update(Graphics g):调用paint()方法,刷新组件外观
- repaint():调用update()方法,刷新组件外观
上面三个方法依次是后者调用前者。
Container类中的update()方法先以组件的背景色填充整个组件区域,然后调用paint()方法重画组件。
Container类的update()方法代码如下:
public void update(Graphics g){
if (isShowing()){
//以组件的背景色填充整个组件区域
if (!(peer instanceof LightweightPeer)){
g.clearRect(0, 0, width, height);
}
paint(g);
}
}
普通组件的update()方法则直接调用paint()方法。
程序不应该主动调用组件的paint()和update()方法,它们都由AWT系统负责调用。如果程序希望AWT系统重新绘制该组件,则调用该组件的repaint()方法即可。而paint()和update()方法通常被重写。
重写update()或paint()方法时,该方法里包含了一个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()方法来实现绘图。
import java.awt.*;
import java.util.*;
public class Test {
MyCanvas drawArea = new MyCanvas();
Frame f = new Frame("画图");
public void init(){
Panel p = new Panel();
drawArea.setPreferredSize(new Dimension(250,180));
f.add(drawArea);
f.add(p, BorderLayout.SOUTH);
f.pack();
f.setVisible(true);
drawArea.repaint();//repaint()会自动调用 paint()方法
}
public static void main(String[] args) {
new Test().init();
}
class MyCanvas extends Canvas {
//重写Canvas的paint()方法,实现绘画
public void paint(Graphics g) {
Random rand = new Random();
//设置画笔颜色
g.setColor(new Color(220, 100, 80));
//随机绘制一个矩形框
g.drawRect(rand.nextInt(200), rand.nextInt(120), 40, 60);
}
}
}
Java也可开发一些动画,即间隔一定时间(通常小于0.1秒)重新绘制新的图像,两次绘制的图像之间的差异很小,肉眼看起来就成了所谓的动画。为了实现间隔一定的时间就重新调用组件的repaint()方法,可以借助于Swing类提供的Timer类。
Timer类是一个定时器,有如下构造器:
- Timer(int delay, ActionListener listener):每间隔delay毫秒,系统自动触发ActionListener监听器里的一鸣惊人监听器大(actionPerformed()方法)。
下面程序示范一个简单的弹球游戏,其中小球和球拍分别以圆形区域和矩形区域代替,小球开始以随机速度向下运动,遇到边框或球拍时小球反弹;球拍则由用户控制,
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Random;
import javax.swing.Timer;
public class Test {
//桌面的宽度和高度
private final int TABLE_WIDTH = 300;
private final int TABLE_HEIGHT = 400;
//球拍的垂直位置
private final int RACKET_Y = 340;
//球拍的高度和宽度
private final int RACKET_HEIGHT = 20;
private final int RACKET_WIDTH = 60;
//小球的大小
private final int BALL_SIZE = 16;
private Frame f = new Frame("弹球游戏");
Random rand = new Random();
//小球纵向运行速度
private int ySpeed = 10;
//返回一个-0.5~0.5的比率,用于控制小球的方向
private double xyRate = rand.nextDouble() - 0.5;
//小球横向运行速度
private int xSpeed = (int) (ySpeed * xyRate * 2);
//小球坐标
private int ballX = rand.nextInt(200) + 20;
private int ballY = rand.nextInt(10) + 20;
//rackedX代表球拍的水平位置
private int racketX = rand.nextInt(200);
private MyCanvas tableArea = new MyCanvas();
Timer timer;
//游戏是否结束的旗标
private boolean isLose = false;
public void init() {
//设置桌面区域的最佳大小
tableArea.setPreferredSize(new Dimension(TABLE_WIDTH, TABLE_HEIGHT));
f.add(tableArea);
//定义键盘监听器
KeyAdapter keyProcessor = new KeyAdapter() {
public void keyPressed(KeyEvent ke) {
//按下向左、向右键时,球拍水平坐标分别减少、增加
if (ke.getKeyCode() == KeyEvent.VK_LEFT) {
if (racketX > 0) {
racketX -= 10;
}
}
if (ke.getKeyCode() == KeyEvent.VK_RIGHT) {
if (racketX < TABLE_WIDTH - RACKET_WIDTH) {
racketX += 10;
}
}
}
};
//为窗口和tableArea对象分别添加键盘监听器
f.addKeyListener(keyProcessor);
tableArea.addKeyListener(keyProcessor);
//定义每0.1秒执行一次的事件监听器
ActionListener taskPerformer = new ActionListener() {
public void actionPerformed(ActionEvent evt) {
//如果小球碰到左边框、右边框
if (ballX <= 0 || ballX >= TABLE_WIDTH - BALL_SIZE) {
xSpeed = -xSpeed;
}
//如果小球高度超出了球拍位置,且横向不在球拍范围之内,游戏结束
if (ballY >= RACKET_Y - BALL_SIZE && (ballX < racketX || ballX > racketX + RACKET_WIDTH)) {
timer.stop();
//设置游戏是否结束的旗标为true
isLose = true;
tableArea.repaint();
}
//如果小球到顶部或者位于球拍之内,且达到球拍位置,小球反弹
else if (ballY <= 0 || (ballY >= RACKET_Y - BALL_SIZE && ballX > racketX && ballX <= racketX + RACKET_WIDTH)) {
ySpeed= -ySpeed;
}
//小球坐标增加
ballY += ySpeed;
ballX += xSpeed;
tableArea.repaint();
}
};
timer = new Timer(100, taskPerformer);
timer.start();
f.pack();
f.setVisible(true);
}
public static void main(String[] args) {
new Test().init();
}
class MyCanvas extends Canvas{
//重写Canvas的paint()方法,实现绘画
public void paint(Graphics g) {
//如果游戏已经结束
if (isLose) {
g.setColor(new Color(255, 0, 0));
g.setFont(new Font("Times", Font.BOLD, 30));
g.drawString("游戏已结束!", 50, 200);
}
//如果游戏未结束
else {
//设置颜色并绘制小球
g.setColor(new Color(240, 240, 80));
g.fillOval(ballX, ballY, BALL_SIZE, BALL_SIZE);
//设置颜色
g.setColor(new Color(80, 80, 200));
g.fillRect(racketX, RACKET_Y, RACKET_WIDTH, RACKET_HEIGHT);
}
}
}
}
游戏有轻微闪烁,这是由于AWT组件的绘图没有采用双缓冲技术,当重写paint()方法来绘制图形时,所有图形都是直接绘制到GUI组件上的,所以多次调用paint()进行绘制会发生闪烁现象。使用Swing组件就可避免这种闪烁。Swing组件没有提供Canvas对应的组件,使用Swing的Panel组件作为画布即可。
AWT允许在组件上绘制位图,Graphics提供了drawImage方法用于绘制位图,该方法需要一个Image参数代表位图。
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利用ImageReader和ImageWriter读写图形文件。
- static String[] getReaderFileSuffixes():返回ImageIO所有能读的图形文件后缀
- static String[] getReaderFormatNames():返回ImageIO所有能读的图形文件的非正式格式名称
- static String[] getWriterFileSuffixes()
- static String[] getWriterFormatNames() 同上
Image image = ImageIO.read(new File("image/board.jpg"));
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实例。
//创建系统剪贴板
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
//创建本地剪贴板:
Clipboard clipboard = new Clipboard("cb");
<2>将需要放入剪贴板中的字符串封装成StringSelection对象
StringSelection st = new StringSelection(targetStr);
<3>调用剪贴板对象的setContents()方法将StringSelection放进剪贴板中,该方法有两个参数,Transferable和ClipboardOwner对象,通常把第二个参数设为null
clipboard.setContents(st, null);
从剪贴板中取出数据,调用Clipboard对象的getDate(DataFlavor flavor)方法。如果指定flavor的数据不存在,该方法将引发UnsupportedFlavorException异常。为免出现异常,可以先调用Clipboard对象的isDataFlavorAvailable(DataFlavor flavor)来判断指定flavor的数据是否存在。
getData方法返回的是Object类型,需强制转型。
if (clipboard.isDateFlavorAvailable(DateFlavor.stringFlavor)){
String content = (String)clipboard.getData(DateFlavor.stringFlavor);
}
要将图像放入剪贴板内,则必须提供一个Transferable接口的实现类。该实现类封装一个image对象,并且向外表现为imageFlavor内容
public class ImageSelection implements Transferable{
private Image image;
//构造器
public ImageSelection(Image image){
this.image = image;
}
//返回该Transferable对象所支持的所有DateFlavor
public DateFlavor[] getTransferDataFlavors(){
return new DataFlavor[]{DataFlavor.imageFlavor};
}
//取出该Transferable对象里实际的数据
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException{
if (flavor.equals(DataFlavor.imageFlavor)){
return image;
}
else{
throw new UnsupportedFlavorException(flavor);
}
}
//返回该Transferable对象是否支持指定的DataFlavor
public boolean isDataFlavorSupported(DataFlavor flavor){
return flavor.equals(DataFlavor.imageFlavor);
}
}
有了以上ImageSelection封装类后,可以用下面的语句操作Image对象放入和取出剪贴板
//将image对象封装成ImageSelection对象
ImageSelection contents = new ImageSelection(image);
//将ImageSelection对象放入剪贴板中
clipboard.setContents(contents, null);
//取出
if (clipboard.isDataFlavorAvailable(DataFlavor.imageFlavor)){
try{
//取出剪贴板中的imageFlavor内容
myImage =(Image)clipboard.getData(DataFlavor.imageFlavor));
}
catch (Exception e){
e.printStackTrace();
}
}
本地剪贴板可以保存任何类型的Java对象。为了将任意类型的Java对象保存到剪贴板中,DataFlavor里提供了一个javaJVMLocalObjectMimeType的常量,该常量 是一个MIME类型字符串:application/x-java-jvm-local-objectref,将Java对象放入本地剪贴板中必须使用该MIME类型。该MIME类型表示仅将对象引用复制到剪贴板中,对象引用只有在同一个虚拟机中才有效。
Java并没有提供封装对象引用的Transferable实现类,因此必须自己实现该接口。
public class LocalObjectSelection implements Transferable{
//持有一个对象的引用
private Object obj;
//构造器
public LocalObjectSelection(Object obj){
this.obj = obj
}
//返回该Transferable对象支持的DataFlavor
public DataFlavor[] getTransferDataFlavors(){
DataFlavor[] flavors = new DataFlavor[2];
//获取被封装对象的类型
Class clazz = obj.getClass();
String mimeType = "application/x-java-jvm-local-objectref;" + "class=" + clazz.getName();
try{
flavors[0] = new DataFlavor(mimeType);
flavors[1] = DataFlavor.stringFlavor;
return flavors;
}
catch (ClassNotFoundException e){
e.printStackTrace();
return null;
}
}
//取出该Transferable对象封装的数据
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException{
if(!isDataFlavorSupported(flavor)){
throw new UnsupportedFlavorException(flavor);
}
if(flavor.equals(DataFlavor.stringFlavor)){
return obj.toString();
}
return obj;
}
public boolean isDataFlavorSupported(DataFlavor flavor){
return flavor.equals(DataFlavor.stringFlavor) || flavor.getPrimaryType().equals("application") && flavor.getSubType().equals("x-java-jvm-local-objectref") && flavor.getRepresentationClass().isAssignableFrom(obj.getClass());
}
}
上面程序创建了一个DataFlavor对象,用于表示本地Person对象引用的数据格式。创建DataFlavor对象可以使用如下构造器:
DataFlavor(String mimeType):根据mimeType字符串构造DataFlavor
有了上面的LocalObjectSelection封装类后,就可以使用该封装类来封装某个对象的引用,从而将该对象的引用放入本地剪贴板中。
Person p = new Person(name, age);
//将Person对象封装成LocalObjectSelection对象
LocalObjectSeletion ls = new LocalObjectSelection(p);
//将LocalObjectSelection对象放入本地剪贴板中
clipboard.setContents(ls, null);
//取出
//创建保存Person对象引用的DataFlavor对象
DataFlavor personFlavor = new DataFlavor("application/x-java-jvm-local-objectref;class=Person");
//取出本地剪贴板中的内容
if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)){
Person p = (Person)clipboard.getData(personFlavor);
}
系统剪贴板支持传输序列化的Java对象和远程对象,复制到剪贴板中的序列化的Java对象和远程对象可以使用另一个Java程序(不在同一个虚拟机内的程序)来读取。DataFlavor中提供了javaSerializedObjectMimeType、javaRemoteObjectMimeType两个字符串常量来表示序列化的Java对象和远程对象的MIME类型,这两种MIME对象提供了复制对象、读取对象所包含的复杂操作,程序只需创建对应的Transferable实现类即可。
如果某个类是可序列化的,则该类的实例可以转换成二进制流,从而可以将该对象通过网络传输或保存到磁盘上,为了保证某个类是可序列化的,只要让该类实现Serializable接口即可。
public class SerialSelection implements Transferable{
//持有一个可序列化的对象
private Serializable obj;
//创建该类的对象时传入被持有的对象
public SerialSelection(Serializable obj){
this.obj = obj;
}
public DataFlavor[] getTransferDataFlavor(){
DataFlavor[] flavors = new DataFlavor[2];
//获取被封装对象的类型
Class clazz = obj.getClass();
try{
flavors[0] = new DataFlavor(DataFlavor.javaSerializedObjectMimeType + ";class=" + clazz.getName());
flavors[1] = DataFlavor.stringFlavor;
return flavors;
}
catch (ClassNotFoundException e){
e.printStackTrace();
return null;
}
}
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException{
if(!isDataFlavorSupported(flavor)){
throw new UnsupportedFlavorException(flavor);
}
if(flavor.equals(DataFlavor.stringFlavor)){
return obj.toString();
}
return obj;
}
public boolean isDataFlavorSupported(DataFlavor flavor){
return flavor.equals(DataFlavor.stringFlavor) || flavor.getPrimaryType().equals("application") && flavor.getSubType().equals("x-java-serialized-object") && flavor.getRepresentationClass().isAssignableFrom(obj.getClass());
}
}
AWT提供了对拖放源和拖放目标的支持,分别由DragSource和DropTarget两个类来表示。
拖放操作中被传递的目标也用Transferable接口来封装,同样使用DataFlavor来表示被传递的数据格式
AWT提供了DropTarget类来表示拖放目标,通过该类的构造器来创建一个拖放目标:
- DropTarget(Component c, int ops, DropTargetListener dtl):将组件c创建成一个拖放目标,默认可接受ops值所指定的拖放操作。
ops可接受的值和监听器的事件处理器详见API
下面程序利用拖放目标创建了一个简单的图片浏览工具,当用户把一个或多个图片文件拖入该窗口时,该窗口将会自动打开每个图片文件
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDragEvent;
import java.io.IOException;
public class DropTargetTest{
final int DESKTOP_WIDTH = 480;
final int DESKTOP_HEIGHT = 360;
final int FRAME_DISTANCE = 30;
JFrame jf = new JFrame("测试拖放目标——把图片文件拖入该窗口");
//定义一个虚拟桌面
private JDesktopPane desktop = new JDesktopPane();
//保存下一个内部窗口的坐标点
private int nextFrameX;
private int nextFrameY;
//定义内部窗口为虚拟桌面的1/2大小
private int width = DESKTOP_WIDTH / 2;
private int height = DESKTOP_HEIGHT / 2;
public void init(){
desktop.setPreferredSize(new Dimension(DESKTOP_WIDTH, DESKTOP_HEIGHT));
//将当前窗口创建成拖放目标
new DropTarget(jf, DnDConstants.ACTION_COPY, new ImageDropTargetListener());
jf.add(desktop);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.pack();
jf.setVisible(true);
}
class ImageDropTargetListener extends DropTargetAdapter {
public void drop(DropTargetirDragEvent event) {
//接受复制操作
event.acceptDrag(DnDConstants.ACTION_COPY);
//获取拖放内容
Transferable transferable = event.getTransferable();
DataFlavor[] flavors = transferable.getTransferDataFlavors();
//遍历拖放内容里的所有数据格式
for (int i = 0; i < flavors.length; i++){
DataFlavor d = flavors[i];
try {
//如果拖放内容的数据格式是文件列表
if (d.equals(DataFlavor.javaFileListFlavor)) {
//取出拖放操作里的文件列表
List fileList = (List) transferable.getTransferData(d);
for (Object f : fileList) {
//显示每个文件
showImage((File) f, event);
}
}
}
catch (Exception e) {
e.printStackTrace();
}
//强制拖放操作结束,停止阻塞拖放目标
event.dropComplete(true);
}
}
}
//显示每个文件的工具方法
private void showImage(File f , DropTargetDragEvent event) throws IOException {
Image image = ImageIO.read(f);
if (image == null) {
//强制拖放操作结束,停止阻塞拖放目标
event.dropComplete(true);
JOptionPane.showInternalMessageDialog(desktop, "系统不支持这种类型的文件");
//方法返回,不会继续操作
return;
}
ImageIcon icon = new ImageIcon(image);
//创建内部窗口显示该图片
JInternalFrame iframe = new JInternalFrame(f.getName(), true, true, true, true);
JLabel imageLabel = new JLabel(icon);
iframe.add(new JScrollPane(imageLabel));
desktop.add(iframe);
//设置内部窗口的原始位置(内部窗口默认大小是0x0,放在0,0位置)
iframe.reshape(nextFrameX, nextFrameY, width, height);
//使该窗口可见,并尝试选中它
iframe.show();
//计算下一个内部窗口的位置
nextFrameX += FRAME_DISTANCE;
nextFrameY += FRAME_DISTANCE;
if (nextFrameX + width > desktop.getWidth()) {
nextFrameX = 0;
}
if (nextFrameY + height > desktop.getHeight()) {
nextFrameY = 0;
}
}
public static void main(String[] args) {
new DropTargetTest().init();
}
}
文本格式的拖放内容使用DataFlavor.stringFlavor格式来表示。
带格式的内容处理方法如下
//如果被拖放的内容是text/html格式的输入流
if ( d.isMimeTypeEqual("text/html") && d.getRepresentationClass() == InputStream.class){
String charset = d.getParameter("charset");
InputStreamReader reader = new InputStreamReader(transferable.getTransferData(d), charset);
//使用IO流读取拖放操作的内容
...
}
创建拖放源的步骤如下:
<1>调用DragSource的getDefaultDragSource()方法获得与平台关联的DragSource对象。
<2>调用DragSource对象的createDefaultDragGestureRecognizer(Component c, int action, DragGestureListener dgl)方法将指定组件转换成拖放源。
如下代码将会把一个JLabel对象转换为拖放源
//将srcLabel组件转换为拖放源
dragSource.createDefaultDragGestureRecognizer(srcLabel, DnDconstants.Action_COPY_OR_MOVE, new MyDragGestureListener());
<3>为第2步中的DragGestureListener监听器提供实现类,该实现类需要重写接口里包含的dragGestureRecognized()方法,该方法负责把拖放内容封装成Transferable对象
下面程序示范了如何把一个JLabel转换成拖放源
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
public class DragSourceTest{
JFrame jf = new JFrame("Swing的拖放支持");
JLabel srcLabel = new JLabel("AWT的拖放支持.\n" + "将该文本域的内容拖入其他程序.\n");
public void init(){
DragSource dragSource = DragSource.getDefaultDragSource();
//将srcLabel转换成拖放源,它能接受复制、移动两种操作
dragSource.createDefaultDragGestureRecognizer(srcLabel, DnDConstants.ACTION_COPY_OR_MOVE, new DragGestureListener() {
@Override
public void dragGestureRecognized(DragGestureEvent event) {
//将Jlabel里的文本信息包装成Transferable对象
String txt = srcLabel.getText();
Transferable transferable = new StringSelection(txt);
//继续拖放操作,拖放过程中使用手状光标
event.startDrag(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR), transferable);
}
});
jf.add(new JScrollPane(srcLabel));
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.pack();
jf.setVisible(true);
}
public static void main(String[] args) {
new DragSourceTest().init();
}
}
实际使用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()方法
import javax.swing.UIManager;
public class Test {
public static void main(String[] args) {
for (UIManager.LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
System.out.println("●" + info.getName() + ":\n" + info);
}
}
}
除此之外还有大量的Java爱好者提供了各种开源的LAF。
Swing为所有的AWT组件提供了对应实现(除了Canvas组件之外,因为在Swing中无须继承Canvas组件),通常在AWT组件的组件名前添加“J”就变成了对应的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等
第个Swing组件都有一个对应的UI类。类名总是将J去掉,然后在后面添加UI后缀。UI代理类通常是一个抽象基类,不同的PLAF会有不同的UI代理实现类(也称PLAF实现)。
如需改变程序的外观风格:
try{
//设置使用Motif风格
UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
//通过更新f容器以及f容器里所有组件的UI
SwingUtilities.updateComponentTreeUI(f);
}
catch(Exception e){
e.PrintStackTrace();
}
下面用Swing创建窗口应用
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
public class SwingComponent {
JFrame f = new JFrame("测试");
JButton ok = new JButton("确认");
//定义单选按钮
JRadioButton male = new JRadioButton("男", true);
JRadioButton female = new JRadioButton("女", false);
//ButtonGroup组合上面两个JRadioButton
ButtonGroup bg = new ButtonGroup();
//复选框
JCheckBox married = new JCheckBox("是否已婚?", false);
String[] colors = new String[]{"红色", "绿色", "蓝色"};
//下拉选择框
JComboBox<String> colorChooser = new JComboBox<String>(colors);
//列表选择框
JList<String> colorList = new JList<String>(colors);
//多行文本域
JTextArea ta = new JTextArea(8, 20);
//单行文本域
JTextField name = new JTextField(40);
JMenuBar mb = new JMenuBar();
JMenu file = new JMenu("文件");
JMenu edit = new JMenu("编辑");
JMenuItem newItem = new JMenuItem("新建");
JMenuItem saveItem = new JMenuItem("保存");
JMenuItem exitItem = new JMenuItem("退出");
JCheckBoxMenuItem autoWrap = new JCheckBoxMenuItem("自动换行");
JMenuItem copyItem = new JMenuItem("复制");
JMenuItem pasteItem = new JMenuItem("粘贴");
JMenu format = new JMenu("格式");
JMenuItem commentItem = new JMenuItem("注释");
JMenuItem cancelItem = new JMenuItem("取消注释");
//右键菜单
JPopupMenu pop = new JPopupMenu();
//组合3个风格菜单项
ButtonGroup flavorGroup = new ButtonGroup();
//5个单选按钮,设定程序外观
JRadioButtonMenuItem metalItem = new JRadioButtonMenuItem("Metal风格", true);
JRadioButtonMenuItem nimbusItem = new JRadioButtonMenuItem("Nimbus风格");
JRadioButtonMenuItem windowsItem = new JRadioButtonMenuItem("Windows风格");
JRadioButtonMenuItem classicItem = new JRadioButtonMenuItem("Windows经典风格");
JRadioButtonMenuItem motifItem = new JRadioButtonMenuItem("Motif风格");
public void init() {
//JPanel装载文本框、按钮
JPanel bottom = new JPanel();
bottom.add(name);
bottom.add(ok);
f.add(bottom, BorderLayout.SOUTH);
//JPanel装载下拉选择框,三个JcheckBox
JPanel checkPanel = new JPanel();
checkPanel.add(colorChooser);
//把两个JRadioButton加入ButtomGroup,使它们无法被同时选中。
bg.add(male);
bg.add(female);
//把两个JradioBUtton加入JFrame,使它们能在界面上显示
checkPanel.add(male);
checkPanel.add(female);
checkPanel.add(married);
//垂直Box盛装多行文本域JPanel
Box topLeft = Box.createVerticalBox();
//使用JScrollPane作为普通组件的JViewPort,为了让多行文本域具有滚动条,将它放下JScrollPane容器中
JScrollPane taJsp = new JScrollPane(ta);//TA是多行文本域
topLeft.add(taJsp);
topLeft.add(checkPanel);
//水平Box盛装topLeft、colorList
Box top = Box.createHorizontalBox();
top.add(topLeft);
top.add(colorList);
f.add(top);
//下面开始组合菜单,并添加监听器
//设置快捷键
newItem.setAccelerator(KeyStroke.getKeyStroke('N', InputEvent.CTRL_DOWN_MASK));
newItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
ta.append("用户单击了“新建”菜单");
}
});
file.add(newItem);
file.add(saveItem);
file.add(exitItem);
edit.add(autoWrap);
//添加菜单分隔线
edit.addSeparator();
edit.add(copyItem);
edit.add(pasteItem);
//为commentItem组件添加提示信息,鼠标移上去会有文字提示
commentItem.setToolTipText("将程序代码注释起来");
format.add(commentItem);
format.add(cancelItem);
//使用添加new JMenuItem("-")的方式不能添加菜单分隔符
edit.add(new JMenuItem("-"));
//format菜单组合到edit菜单中,形成二级菜单
edit.add(format);
//file、edit菜单添加到mb菜单条中
mb.add(file);
mb.add(edit);
f.setJMenuBar(mb);
//组合右键菜单
//flavorGroup是ButtonGroup,将5个选项组合在一起,使其只能五选一
flavorGroup.add(metalItem);
flavorGroup.add(nimbusItem);
flavorGroup.add(windowsItem);
flavorGroup.add(classicItem);
flavorGroup.add(motifItem);
//添加到右键菜单
pop.add(metalItem);
pop.add(nimbusItem);
pop.add(windowsItem);
pop.add(classicItem);
pop.add(motifItem);
//为5个风格菜单创建事件监听器
ActionListener flavorListener = new ActionListener() {
public void actionPerformed(ActionEvent e) {
try {
switch (e.getActionCommand()) {
case "Metal风格":
changeFlavor(1);
break;
case "Nimbus风格":
changeFlavor(2);
break;
case "Windows风格":
changeFlavor(4);
break;
case "Windows经典风格":
changeFlavor(5);
break;
}
}
catch (Exception ee) {
ee.printStackTrace();
}
}
};
//为5个风格菜单项添加事件监听器
metalItem.addActionListener(flavorListener);
nimbusItem.addActionListener(flavorListener);
windowsItem.addActionListener(flavorListener);
classicItem.addActionListener(flavorListener);
motifItem.addActionListener(flavorListener);
motifItem.addActionListener(flavorListener);
//调用该方法即可设置右键菜单,无须使用事件机制
ta.setComponentPopupMenu(pop);//ta是多行文本域
//设置关闭窗口时,退出程序
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.pack();
f.setVisible(true);
}
//定义一个方法,用于改变界面风格
private void changeFlavor(int flavor) throws Exception{
switch (flavor){
case 1:
UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
break;
case 2:
UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel");
break;
case 3:
UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
break;
case 4:
UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel");
break;
case 5:
UIManager.setLookAndFeel("com.sun.java.swimg.plaf.motif.MotifLookAndFeel");
break;
}
//更新f窗口内顶级容器以及内部所有组件的UI,注意这里更新的不是JFrame本身,详细介绍见下文
SwingUtilities.updateComponentTreeUI(f.getContentPane());
//更新mb菜单条内部所有组件的UI
SwingUtilities.updateComponentTreeUI(mb);
//更新pop右键菜单以及内部所有组件的UI
SwingUtilities.updateComponentTreeUI(pop);
}
public static void main(String[] args) {
//设置Swing窗口使用Java风格
//JFrame.setDefaultLookAndFeelDecorated(true);
new SwingComponent().init();
}
}
Swing专门为菜单项、工具按钮之间的分隔符提供了一个JSeparator类,通常使用JMenu或者JPopupMenu的addSeparator()方法来创建并添加JSeparator对象,而不是直接使用JSeparator,实际上JSeparator可以用在任何需要使用分隔符的地方
为Swing菜单项指定快捷键必须通过setAccelerator(KeyStroke ks)方法来设置。
JFrame提供了一个getContentPane()方法,用于返回该JFrame的顶级容器(即JRootPane对象),这个顶级容器会包含JFrame所显示的所有非菜单组件。所有看似放在JFrame中的Swing组件,除菜单之外,其实都是放在JFrame对应的顶级容器中的。当程序调用JFrame的add()和setLayout()等方法时,实际上是对JFrame的顶级容器进行操作。
为Swing组件添加右键菜单只须调用setComponentPopupMenu()方法。
单击右上角的X退出程序,只要调用setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)方法即可。
JScrollPane组件不同于普通容器,空甚至不能指定自己的布局管理器,它主要用于为其他的Swing组件提供滚动条支持,JScrollPane通常由普通的Swing组件,可选的垂直、水平滚动条以及可选的行、列标题组成。简而言之,如果希望让JTextArea、JTable等组件有滚动条支持,只要将该放入JScrollPane中,再将JScrollPane容器添加到窗口。
JTable放在JScrollPane容器中才可以显示出JTable组件的标题栏。
可以调用JComponent提供的setBorder(Border b)方法为Swing组件设置边框,其中Border是Swing提供的一个接口用于代表组件的边框,它有数量众多的实现类。
TitledBorder和CompoundBorder比较独特,其中TitledBorder的作用并不是为其他组件添加边框,而是为其设置标题;而CompoundBorder用于组合两个边框。
Swing还提供了一个BorderFactory静态工厂类,提供大量的静态工厂方法用于返回Border实例。
Border还提供了MetalBorders.oolBarBorder、MetalBorders.TextFieldBorder等实现类,用作Swing组件的默认边框,程序中通常无须使用这些系统边框
为Swing组件添加边框步骤:
- 使用BorderFactory或XxxBorder创建XxxBorder实例
- 调用Swing组件的setBorder(Border b)方法为该组件设置边框。
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
public class Test{
private JFrame jf = new JFrame("测试边框");
public void init(){
jf.setLayout(new GridLayout(2,4));
//BevelBorder双线斜面边框
Border bb = BorderFactory.createBevelBorder(BevelBorder.RAISED, Color.RED, Color.GREEN, Color.BLUE, Color.GRAY);
//自定义方法getPanelWithBorder
jf.add(getPanelWithBorder(bb,"BevelBorder\n双线斜面边框"));
//LineBorder单色、任意厚度边框
Border lb = BorderFactory.createLineBorder(Color.ORANGE, 10);
jf.add(getPanelWithBorder(lb, "LineBorder\n单色、任意厚度边框"));
//EmptyBorder占用空间但不执行绘制的空透明边框
Border eb = BorderFactory.createEmptyBorder(20, 5, 10, 30);
jf.add(getPanelWithBorder(eb, "EmptyBorder\n占用空间但不执行绘制的空透明边框"));
//EtchedBorder浮雕化边框
Border etb = BorderFactory.createEtchedBorder(EtchedBorder.RAISED, Color.RED, Color.GREEN);
jf.add(getPanelWithBorder(etb, "EtchedBorder\n浮雕化边框"));
//TitledBorder
TitledBorder tb = new TitledBorder(lb, "测试标题", TitledBorder.LEFT, TitledBorder.BOTTOM, new Font("StSong", Font.BOLD, 18), Color.BLUE);
jf.add(getPanelWithBorder(tb,"TitledBoredr"));
//MatteBorder类似衬边的边框
MatteBorder mb = new MatteBorder(20, 5, 10, 30, Color.GREEN);
jf.add(getPanelWithBorder(mb,"MatteBorder\n类似衬边的边框"));
//CompoundBorder将两边框合成新边框,第一个参数代表的边框在外
CompoundBorder cb = new CompoundBorder(new LineBorder(Color.RED, 8), tb);
jf.add(getPanelWithBorder(cb, "CompoundBorder\n将两边框合成新边框"));
jf.pack();
jf.setVisible(true);
}
public static void main(String[] args){
new Test().init();
}
public JPanel getPanelWithBorder(Border b, String BorderName){
JPanel p = new JPanel();
p.add(new JLabel(BorderName));
p.setBorder(b);
return p;
}
}
- 所有Swing组件默认启用双缓冲绘图技术
- 所有Swing组件都提供了简单的键盘驱动
双缓冲技术能改进频繁重绘GUI组件的显示效果(避免闪烁现象)。关闭双缓冲可以在组件上调用setDoubleBuffered(false)方法。
JComponent类提供了getInputMap()和getActionMap()两个方法。其中getInputMap()返回一个InputMap对象,该对象用于将KeyStroke对象(代表键盘或其他类似输入设备的一次输入事件)和名字关联;getActionMap()返回一个ActionMap对象,该对象用于将指定名字和Action(Action接口是ActionListener接口的子接口,可作为一个事件监听器使用)关联,从而可以允许用户通过键盘操作来替代鼠标驱动GUI上的Swing组件,相当于为GUI组件提供快捷键。用法如下:
component.getInputMap().put(aKeyStroke, aCommand);
component.getActionMap().put(aCommand, anAction);
例子:
Action sendMsg = new AbstractAction(){
public void actionPerformed(ActionEvent e){
//指定操作
}
};
jBottom.addActionLinstener(senMsg);
jTextField.getInputMap().put(KeyStroke.getKeyStroke('n', java.awt.event.InputEvent.Ctrl_MASK), "send");
jTextField.getActionMap().put("send", sendMsg);
采用这种键盘事件机制,无须为Swing组件绑定键盘监听器,从而可以复用键盘按键单击事件的事件监听器。
构造器JToolBar(String name, int orientation) ,可以指定0、1和2个参数。orientation为工具条的方向。如果无参数,则默认为HORIZONTAL
常用方法:
- JButton add(Action a):添加一个指派动作的新的JButton。
- void addSeparator(Dimension size):将指定大小的分隔符添加到工具栏的末尾,无参数则为默认大小
- void setFloatable(boolean b):如果要移动工具栏,此属性必须设置为true
- void setMargin(Insets m):设置工具栏边框和它的按钮之间的空白。
- void setOrientation(int o):设置工具栏的方向
- void setRollover(boolean rollover):如果 rollover 状态为true,则仅当鼠标指针悬停在工具栏按钮上时,才绘制该工具栏按钮的边框。此属性的默认值为false。
更多方法见API。
Action本身并不是按钮,但它被添加到某些容器(或直接使用Action来创建按钮)时,这些容器会为该Action创建对应的组件(菜单和按钮,使用该Action的name和icon)。只有处于激活状态的Action所对应的Swing组件才可以响应用户动作。系统为该Action创建的所有组件注册同一个事件监听器,即它自身的actionPerformed()方法。
例如,程序中有一个菜单项、一个工具按钮、还有一个普通按钮都需要完成某个“复制”动作,程序就可以将该“复制”动作定义成Action,并为之指定name和icon属性,然后通过该Action来创建菜单项、工具按钮和普通按钮,就可以让这三个组件具有相同的功能。另一个“粘贴”按钮也大致相似,而且“粘贴”组件默认不可用,只有当“复制”组件被触发后,且剪贴板中有内容时才可用。
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
public class Test{
JFrame jf = new JFrame("测试工具条");
JTextArea jta = new JTextArea(6,35);
JToolBar jtb = new JToolBar();
JMenuBar jmb = new JMenuBar();
JMenu edit = new JMenu("编辑");
//获取系统剪贴板
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
//创建“粘贴”Action,用于创建菜单项、工具按钮和普通按钮
Action pasteAction = new AbstractAction("粘贴") {
@Override
public void actionPerformed(ActionEvent e) {
//如果剪贴板中包含stringFlavor内容
if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
try {
//取出剪贴板中的stringFlavor内容
String content = (String) clipboard.getData(DataFlavor.stringFlavor);
//将选中内容替换成剪贴板中的内容
jta.replaceRange(content, jta.getSelectionStart(), jta.getSelectionEnd());
}
catch (Exception ee){
ee.printStackTrace();
}
}
}
};
//创建“复制”Action
Action copyAction = new AbstractAction("复制") {
@Override
public void actionPerformed(ActionEvent e) {
StringSelection contents = new StringSelection(jta.getSelectedText());
//将StringSelection对象放入剪贴板
clipboard.setContents(contents, null);
//如果剪贴板中包含stringFlavor内容
if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
//将pasteAction激活
pasteAction.setEnabled(true);
}
}
};
public void init() {
//pasteAction默认处于不激活状态
pasteAction.setEnabled(false);
jf.add(new JScrollPane(jta));
//以Action创建按钮,并添加到Panel中
JButton copyBn = new JButton(copyAction);
JButton pasteBn = new JButton(pasteAction);
JPanel jp = new JPanel();
jp.add(copyBn);
jp.add(pasteBn);
jf.add(jp, BorderLayout.SOUTH);
//向工具条添加Action对象,该对象会转换成工具按钮
jtb.add(copyAction);
jtb.addSeparator();
jtb.add(pasteAction);
//向菜单中添加Action对象,该对象会变成菜单项
edit.add(copyAction);
edit.add(pasteAction);
jmb.add(edit);
jf.setJMenuBar(jmb);
//设置工具条和工具按钮之间的页边距
jtb.setMargin(new Insets(20, 10, 5, 30));
//向窗口添加工具条
jf.add(jtb, BorderLayout.NORTH);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.pack();
jf.setVisible(true);
}
public static void main(String[] args) {
new Test().init();
}
}
JColorChooser用于创建颜色选择器对话框,主要方法有:
- static JDialog createDialog(Component c, String title, boolean modal, JColorChooser chooserPane, ActionListener okListener, ActionListener cancelListener):创建并返回包含指定 ColorChooser 窗格及 "OK"、"Cancel" 和 "Reset" 按钮的新对话框。 如果按下 "OK" 或 "Cancel" 按钮,则对话框自动隐藏(但未释放)。如果按下 "Reset" 按钮,则将颜色选取器的颜色重置为上一次在对话框上调用 show 时设置的颜色,并且对话框仍然显示。modal - 一个 boolean,为 true 时,在关闭对话框之前,程序的剩余部分将一直处于非激活状态。
简单绘图程序:
import javax.swing.*;
import java.awt.*;
import java.awt.Color;
import java.awt.event.*;
import java.awt.image.BufferedImage;
public class Test{
private final int AREA_WIDTH = 500;
private final int AREA_HEIGHT = 400;
//上一次鼠标手动事件的鼠标坐标
private int preX = -1;
private int preY = -2;
//右键菜单设置画笔颜色
JPopupMenu pop = new JPopupMenu();
JMenuItem chooseColor = new JMenuItem("Choose Color");
BufferedImage image = new BufferedImage(BufferedImage.TYPE_INT_ARGB, AREA_HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
private JFrame f = new JFrame("简单手绘程序");
//自定义的子类的实例
private DrawCanvas drawArea = new DrawCanvas();
private Color foreColor = new Color(255, 0, 0);
public void init(){
chooseColor.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent ae) {
//下面代码弹出模式的颜色选择对话框,并返回用户选择的颜色
// foreColor = JColorChooser.showDialog(f, "选择画笔颜色", foreColor);
//下面代码弹出一个非模式的颜色选择对话框
final JColorChooser colorPane = new JColorChooser(foreColor);
JDialog jd = JColorChooser.createDialog(f, "选择画笔颜色", false, colorPane, new ActionListener() {
public void actionPerformed(ActionEvent ae) {
foreColor = colorPane.getColor();
}
}, null);
jd.setVisible(true);
}
});
//将菜单项组合成右键菜单
pop.add(chooseColor);
drawArea.setComponentPopupMenu(pop);
g.fillRect(0, 0, AREA_WIDTH, AREA_HEIGHT);
drawArea.setPreferredSize(new Dimension(AREA_WIDTH, AREA_HEIGHT));
//监听鼠标移动动作
drawArea.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
if (preX > 0 && preY > 0) {
g.setColor(foreColor);
g.drawLine(preX, preY, e.getX(), e.getY());
}
drawArea.repaint();
//将当前鼠标事件点的X、Y坐标保存起来
preX = e.getX();
preY = e.getY();
}
});
//监听鼠标事件
drawArea.addMouseMotionListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
preX = -1;
preY = -1;
}
});
f.add(drawArea);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.pack();
f.setVisible(true);
}
public static void main(String[] args){
new Test().init();
}
class DrawCanvas extends JPanel{
public void paint(Graphics g) {
g.drawImage(image, 0, 0, null);
}
}
}
JFileChooser的功能与AWT中的FileDialog基本相似,也是用于生成“打开文件”、“保存文件”对话框,它无须依赖于本地平台的GUI。
构造器详见API
JFileChooser并不是JDialog的子类,所以不能使用setVisible(true)方法来显示该文件对话框,而是调用showXxxDialog()方法。
使用JFileChooser创建对话框步骤如下:
<1>采用构造器创建一个JFileChooser对象,该对象无须指定parent组件,这意味着它可以在多个窗口中共用。创建时可以指定初始化路径:
//以当前路径创建文件选择器
JFileChooser chooser = new JFileChooser(".");
<2>调用JFileChooser的一系列可选方法执行初始化操作。常用方法如下:
- setSelectedFile(File file)
- getSelectedFiles():设置选中的文件(或文件列表)。如果该文件的父目录不是当前目录,则将当前目录更改为该文件的父目录。
- setMultiSelectionEnabled(boolean b):设置文件选择器,以允许选择多个文件。默认false
- setFileSelectionMode(int mode):设置只选择文件、只选择目录,或者可选择文件和目录。默认值是JFilesChooser.FILES_ONLY。参数:mode - 要显示的文件类型: JFileChooser.FILES_ONLY,JFileChooser.DIRECTORIES_ONLY,JFileChooser.FILES_AND_DIRECTORIES
- public void setAcceptAllFileFilterUsed(boolean b):确定是否将 AcceptAll FileFilter 用作可选择过滤器列表中一个可用选项。如果为 false则取消“文件类型”下拉列表中的“所有文件”选项
<3>如果让文件对话框实现文件过滤功能,则需要结合FileFilter类。JFileChooser有两个方法来安装文件过滤器。
- addChoosableFileFilter(FileFilter filter):该方法允许该对话框有多个文件过滤器
- setFileFilter(FileFilter filter):该方法导致该文件对话框只有一个文件过滤器
<4>如有需要可以结合FileView类来改变对话框中文件的视图外观
<5>调用ShowXxxDialog方法打开文件对话框。通常用如下三个方法
- int showDialog(Component parent, String approveButtonText):弹出具有自定义 approve 按钮(默认是“保存”或“打开”)的自定义文件选择器对话框。
- int showOpenDialog(Component parent):弹出一个 "Open File" 文件选择器对话框。
- int showSaveDialog(Component parent):弹出一个 "Save File" 文件选择器对话框。
关闭对话框时返回一个int类型的值,分别是:JFileChooser.CANCEL_OPTION、JFileChooser.APPROVE_OPTION、JFileChooser.ERROR_OPTION。如果我们希望获得用户选择的文件,通常应该先判断对话框的返回值是否为JFileChooser.APPROVE_OPTION。
<6>JFileChooser提供了如下两个方法来获取用户选择的文件或文件集:
- File getSelectedFile():返回选中的文件。可由程序员通过 setFile 或者通过用户操作(如在 UI 中键入文件名,或者从 UI 中的列表内选择文件)来进行此设置。
- File[] getSelectedFiles():如果将文件选择器设置为允许选择多个文件,则返回选中文件的列表。
使用FileFilter类进行文件过滤
Java在java.io包下提供了一个FileFilter接口,主要用于作为File类的listFiles(FileFilter)方法的参数。不过此处需要使用位于javax.swing.filechooser包下的FileFilter抽象类。该抽象类包含两个方法:
- abstract boolean accept(File f):此过滤器是否接受给定的文件。
- abstract String getDescription():此过滤器的描述。
如果要使用FileFilter类,则通常需要扩展它,并重写以上两个抽象方法。
重写accept()方法:
public boolean accept(File f){
//如果该文件是路径,则接受该文件
if ( f.isDirectory() ) return true;
//只接受以.gif作为后缀的文件
if (name.endsWith(".gif")){
return true;
}
return false;
}
使用FileView类改变文件对话框中的文件视图风格
FileView类也是一个抽象类,通常需要扩展它,并有选择性地重写它所包含的如下几个抽象方法。
- String getDescription(File f):文件的可读描述。
- Icon getIcon(File f):表示JFileChooser中此文件的图标。
- String getName(File f):文件名称。
- String getTypeDescription(File f):文件类型的可读描述。
- Boolean isTraversable(File f):目录是否是可遍历的。
重写这些方法实际上是为文件选择对话框指定自定义的外观风格。通常可以通过重写getIcon()方法来改变文件对话框中的文件图标
下面是一个简单的图片查看工具程序:
import javax.swing.*;
import javax.swing.filechooser.FileFilter;
import javax.swing.filechooser.FileView;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.util.ArrayList;
public class ImageViewer{
//定义图片预览组件的大小
final int PREVIEW_SIZE = 100;
JFrame jf = new JFrame("简单图片查看器");
JMenuBar menuBar = new JMenuBar();
//该label用于显示图片
JLabel label = new JLabel();
//以当前路径创建文件选择器
JFileChooser chooser = new JFileChooser(".");
JLabel accessory = new JLabel();
//定义文件过滤器
ExtensionFileFilter filter = new ExtensionFileFilter();
public void init() {
//下面开始初始始化JFileChooser的相关属性
//创建一个FileFilter
filter.addExtension("jpg");
filter.addExtension("jpeg");
filter.addExtension("gif");
filter.addExtension("png");
filter.setDescription("图片文件(*.jpg, *.jpeg, *.gif, *.png)");
chooser.addChoosableFileFilter(filter);
//禁止“文件类型”下拉列表中显示“所有文件”选项
chooser.setAcceptAllFileFilterUsed(false);
//为文件选择器指定自定义的FileView对象
chooser.setFileView(new FileIconView(filter));
//为文件选择器指定一个预览图片的附件
chooser.setAccessory(accessory);
//设置预览图片组件的大小和边框
accessory.setPreferredSize(new Dimension(PREVIEW_SIZE, PREVIEW_SIZE));
accessory.setBorder(BorderFactory.createEtchedBorder());
//用于检测被选择文件的改变事件
chooser.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent event) {
//JFileChooser的被选文件已经发生了改变
if (event.getPropertyName() == JFileChooser.SELECTED_FILE_CHANGED_PROPERTY) {
//获取用户选择的文件
File f = (File) event.getNewValue();
if (f == null) {
accessory.setIcon(null);
return;
}
//将所选文件读入ImageIcon对象中
ImageIcon icon = new ImageIcon(f.getPath());
//如果图像太大,则缩小它
if (icon.getIconWidth() > PREVIEW_SIZE) {
icon = new ImageIcon(icon.getImage().getScaledInstance(PREVIEW_SIZE, -1, Image.SCALE_DEFAULT));
}
//改变accessory Label的图标
accessory.setIcon(icon);
}
}
});
//下面代码开始为该窗口安装菜单
JMenu menu = new JMenu("文件");
menuBar.add(menu);
JMenuItem openItem = new JMenuItem("打开");
menu.add(openItem);
//单击openItem菜单项显示“打开文件”对话框
openItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent event) {
//设置文件对话框的当前路径
//chooser.setCurrentDirectory(new File("."));
//显示文件对话框
int result = chooser.showDialog(jf, "打开图片文件");
//如果用户选择了APPROVE按钮,
if (result == JFileChooser.APPROVE_OPTION) {
String name = chooser.getSelectedFile().getPath();
//显示指定图片
label.setIcon(new ImageIcon(name));
}
}
});
JMenuItem exitItem = new JMenuItem("Exit");
menu.add(exitItem);
//为退出菜单绑定事件监听器
exitItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
});
jf.setJMenuBar(menuBar);
//添加用于显示图片的JLabel组件
jf.add(new JScrollPane(label));
jf.pack();
jf.setVisible(true);
}
public static void main(String[] args) {
new ImageViewer().init();
}
}
//创建FileFilter的子类,用以实现文件过滤功能
class ExtensionFileFilter extends FileFilter {
private String description;
private ArrayList<String> extensions = new ArrayList<>();
//自定义方法,用于添加文件扩展名
public void addExtension(String extension) {
if (!extension.startsWith(".")) {
extension = "." + extension;
extensions.add(extension.toLowerCase());
}
}
//用于设置该文件过滤器的描述文本
public void setDescription(String aDescription){
description = aDescription;
}
//继承FileFilter类必须实现的抽象方法,返回该文件过滤器的描述文本
public String getDescription(){
return description;
}
//继承FileFilter类必须实现的抽象方法,判断该文件过滤器是否接受该文件
public boolean accept(File f) {
//如果该文件是路径,则接受
if (f.isDirectory()) {
return true;
}
//将文件名转为小写,便于比较
String name = f.getName().toLowerCase();
//遍历所有可接受的扩展名,如果扩展名相同,该文件就可接受
for (String extension : extensions) {
if (name.endsWith(extension)) {
return true;
}
}
return false;
}
}
//自定义一个FileView类,用于为指定类型的文件或文件夹设置图标
class FileIconView extends FileView {
private FileFilter filter;
public FileIconView(FileFilter filter) {
this.filter = filter;
}
//重写该方法,为文件、文件夹设置图标
public Icon getIcon(File f) {
if (!f.isDirectory() && filter.accept(f)) {
return new ImageIcon("ico/pict.png");
}
else if (f.isDirectory()) {
//获取所有根路径
//可以保证本地机器上物理存在的任何文件的规范路径名都以此方法返回的根之一开始
File[] fList = File.listRoots();
for (File tmp : fList) {
//如果该路径是根路径
if (tmp.equals(f)) {
return new ImageIcon("ico/dsk.png");
}
}
return new ImageIcon("ico/folder.png");
}
//使用默认图标
else{
return null;
}
}
}
可以创建一些简单的对话框。详见API
JOptionPane所有对话框都是模式的
下面程序允许使用JOptionPane来弹出各种对话框
import javax.swing.*;
import javax.swing.border.EtchedBorder;
import javax.swing.border.TitledBorder;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.peer.ButtonPeer;
import java.util.Date;
public class JOptionPaneTest{
JFrame jf = new JFrame("测试JOptionPane");
//定义6个面板,分别用于定义对话框的几种选项
private ButtonPanel messagePanel;
private ButtonPanel messageTypePanel;
private ButtonPanel msgPanel;
private ButtonPanel confirmPanel;
private ButtonPanel optionsPanel;
private ButtonPanel inputPanel;
private String messageString = "消息区内容";
private Icon messageIcon = new ImageIcon("ico/heart.png");
private Object messageObject = new Date();
private Component messageComponent = new JButton("组件消息");
private JButton msgBn = new JButton("消息对话框");
private JButton confirmBn = new JButton("确认对话框");
private JButton inputBn = new JButton("输入对话框");
private JButton optionBn = new JButton("选项对话框");
public void init() {
JPanel top = new JPanel();
top.setBorder(new TitledBorder(new EtchedBorder(), "对话框的通用选项", TitledBorder.CENTER, TitledBorder.TOP));
top.setLayout(new GridLayout(1, 2));
//消息类型Panel,该Panel中的选项决定对话框的图标
messageTypePanel = new ButtonPanel("选择消息的类型", new String[]{"ERROR_MESSAGE", "INFORMATION_MESSAGE", "WARNING_MESSAGE", "QUESTION_MESSAGE", "PLAIN_MESSAGE"});
//消息内容类型Panel,该Panel中的选项决定对话框消息区的内容
messagePanel = new ButtonPanel("选择消息内容的类型", new String[]{"字符串消息", "图标消息", "组件消息", "普通对象消息", "Object[]消息"});
top.add(messageTypePanel);
top.add(messagePanel);
JPanel bottom = new JPanel();
bottom.setBorder(new TitledBorder(new EtchedBorder(),"弹出不同的对话框", TitledBorder.CENTER, TitledBorder.TOP));
bottom.setLayout(new GridLayout(1, 4));
// 创建用于弹出消息对话框的Panel
msgPanel = new ButtonPanel("消息对话框", null);
msgBn.addActionListener(new ShowAction());
msgPanel.add(msgBn);
//创建用于弹出确认对话框的Panel
confirmPanel = new ButtonPanel("确认对话框", new String[]{"DERAULT_OPTION", "YES_NO_OPTION", "YES_NO_CANCEL_OPTION", "OK_CANCEL_OPTION"});
confirmBn.addActionListener(new ShowAction());
confirmPanel.add(confirmBn);
//创建用于弹出输入的对话框的Panel
inputPanel = new ButtonPanel("输入对话框", new String[]{"单行文本框", "下拉列表选择框"});
inputBn.addActionListener(new ShowAction());
inputPanel.add(inputBn);
// 创建用于弹出选项对话框的Panel
optionsPanel = new ButtonPanel("选项对话框", new String[]{"字符串选项", "图标选项", "对象选项"});
optionBn.addActionListener(new ShowAction());
optionsPanel.add(optionBn);
bottom.add(msgPanel);
bottom.add(confirmPanel);
bottom.add(inputPanel);
bottom.add(optionsPanel);
Box box = new Box(BoxLayout.Y_AXIS);
box.add(top);
box.add(bottom);
jf.add(box);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.pack();
jf.setVisible(true);
}
//根据用户选择返回选项类型
private int getOptionType() {
switch (confirmPanel.getSelection()) {
case "DEFAULT_OPTION":
return JOptionPane.DEFAULT_OPTION;
case "YES_NO_OPTION":
return JOptionPane.YES_NO_OPTION;
case "YES_NO_CANCEL_OPTION":
return JOptionPane.YES_NO_CANCEL_OPTION;
default:
return JOptionPane.OK_CANCEL_OPTION;
}
}
//根据用户选择返回消息
private Object getMessage() {
switch (messagePanel.getSelection()) {
case "字符串消息":
return messageString;
case "图标消息":
return messageIcon;
case "组件消息":
return messageComponent;
case "普通对象消息":
return messageObject;
default:
return new Object[]{messageString, messageIcon, messageObject, messageComponent};
}
}
//根据用户选择返回消息类型(决定图标区的图标)
private int getDialogType() {
switch (messageTypePanel.getSelection()) {
case "ERROR_MESSAGE":
return JOptionPane.ERROR_MESSAGE;
case "INFORMATION_MESSAGE":
return JOptionPane.INFORMATION_MESSAGE;
case "WARNING_MESSAGE":
return JOptionPane.WARNING_MESSAGE;
case "QUESTION_MESSAGE":
return JOptionPane.QUESTION_MESSAGE;
default:
return JOptionPane.PLAIN_MESSAGE;
}
}
private Object[] getOptions() {
switch (optionsPanel.getSelection()) {
case "字符串选项":
return new String[]{"a", "b", "c", "d"};
case "图标选项":
return new Icon[]{new ImageIcon("ico/1.gif"), new ImageIcon("ico/2.gif"), new ImageIcon("ico/3.gif"), new ImageIcon("ico/4.gif")};
default:
return new Object[]{new Date(), new Date(), new Date()};
}
}
//为各类按钮定义事件监听器
private class ShowAction implements ActionListener{
public void actionPerformed(ActionEvent event) {
switch (event.getActionCommand()) {
case "确认对话框":
JOptionPane.showConfirmDialog(jf, getMessage(), "确认对话框", getOptionType(), getDialogType());
break;
case "输入对话框":
if (inputPanel.getSelection().equals("单选文本框")) {
JOptionPane.showInputDialog(jf, getMessage(), "输入对话框", getDialogType());
}
else {
JOptionPane.showInputDialog(jf, getMessage(), "输入对话框", getDialogType(), null, new String[]{"轻量级Java EE企业应用实战", "疯狂Java讲义", "疯狂Java讲义"});
}
break;
case "消息对话框":
JOptionPane.showMessageDialog(jf, getMessage(), "消息对话框", getDialogType());
break;
case "选项对话框“":
JOptionPane.showOptionDialog(jf, getMessage(), "选项对话框", getOptionType(), getDialogType(), null, getOptions(), "a");
break;
}
}
}
public static void main(String[] args){
new JOptionPaneTest().init();
}
}
//定义一个JPanel类扩展类,该类的对象包含多个纵向排列的JRadioButton控件,且Panel扩展类可以指定一个字符串作为TittledBorder
class ButtonPanel extends JPanel{
private ButtonGroup group;
public ButtonPanel(String title, String[] options) {
setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), title));
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
group = new ButtonGroup();
for (int i = 0; options != null && i < options.length; i++) {
JRadioButton b = new JRadioButton(options[i]);
b.setActionCommand(options[i]);
add(b);
group.add(b);
b.setSelected(i == o);
}
}
//定义一个方法,用于返回用户选择的选项
public String getSelection() {
return group.getSelection().getActionCommand();
}
}
用于创建更复杂的界面。
用于创建分割面板,可以将一个组件(通常是容器)分割成两个部分,并提供一个分割条,用户可以手动分割条来调整两个部分的大小。
分割面板实质是一个特殊容器,只能容纳两个组件。
详见API
在窗口上放置多个标签页,每个标签页相当于获得了一个与外部容器具有相同大小的组件摆放区域。
使用JTabbedPane创建标签页步骤如下:
<1> 创建一个JTabbedPane对象
<2>调用addTab()、insertTab()、setComponentAt()、removeTabAt()方法来增加、插入、修改和删除标签页。addTab总在最前面增加标签页。
不管使用哪种操作来改变JTabbedPane中的标签页,都是传入一个Component组件作为标签页。也就是说必须将每页的所有组件都放置到一个容器如JPanel里,再将该容器设置为JTabbedPane指定位置的组件
不要使用add()方法添加组件,它每次添加的标签也直接覆盖原有的标签页
<3>如需显示某个标签页,则调用 setSelectedIndex()方法
<4>用API中的其他方法进行设置
<5>有需要则使用ChangeListener监听器
JLayeredPane是一个代表有层次深度的容器,它允许组件在需要是互相重叠。添加组件时须指定一个深度索引,层次索引越高的组件越靠上
JLayeredPane 还将容器的层次深度分成几个默认层。详见API
JLayeredPane 的子类 JDesktopPane 容器更常用——很多程序都需要启动多个内部容器来显示信息,典型的如Eclipse使用这种内部窗口来分别显示每个Java源文件,这些内部容器都属于同一个外部窗口,当外部窗口最小化时,这些内部窗口都被隐藏起来。Windows中这种用户界面被称为多文档界面(Multiple Document Interface,MDI)。
JDesktopPane 需要和 JInternalFrame 结合使用,其中 JDesktopPane 代表一个虚拟桌面,而 JInternalFrame 则用于创建内部窗口。步骤如下:
<1> 创建一个 JDesktopPane 对象。
<2> 使用 JInternalFrame 创建一个内部窗口。
<3> 设置内部窗口的大小位置并显示:
//同时设置窗口的大小和位置
iframe.reshape(20, 20, 300, 400);
//使该窗口可见,并尝试选中它
iframe.show();
<4> 将内部窗口添加到 JDesktopPane 容器中, 再将 JDesktopPane 容器添加到其他容器中。 JDesktopPane 不能独立存在。
Swing 支持拖放操作的组件:
Swing组件 | 作为拖放源导出 | 作为拖放目标接收 |
---|---|---|
JColorChooser | 颜色对象的本地引用 | 任何颜色 |
JFileChooser | 文件列表 | 无 |
JList | 所选节点的HTML描述 | 无 |
JTable | 所选中的行 | 无 |
JTree | 所选节点的HTML描述 | 无 |
JTextComponent | 所选文本 | 接收文本,其子类JTextArea还可接收文件列表,负责将它打开 |
默认情况下都没有启动拖放支持,可调用这些组件的setDragEnabled(true)方法来启动拖放支持。
除此之外,Swing还提供一种非常特殊的类,TransferHandler,可以直接将某个组件指定属性设置成拖放目标,前提是该组件具有该属性的setter方法。如JTextArea类提供了一个setForeground(Color)方法,我们就可以利用TransferHandler 将foreground定义成拖放目标:
//允许直接将一个Color对象拖入该JTextArea对象,并赋给它的foreground属性
txt.setTransferHandler(new TransferHandler("foreground"));
JLayer 的功能是在指定组件上额外添加一个装饰层,开发者可以在这个装饰层上任意绘制(直接重写paint(Grapchics g, JComponent c)方法)。
JLayer 一般总是要和 LayerUI 一起使用。 后者用于被扩展,扩展 LayerUI 时重写它的paint(Grapchics g, JComponent c)方法,在该方法中绘制的内容会对指定组件进行装饰。
使用 JLayer 只须两行代码:
// 创建LayerUI对象
LayerUI<JComponent> layerUI = new XxxLayerUI();
// 使用layerUI 来装饰指定的JPanel组件
JLayer<JComponent> layer = new JLayer<JComponent>(panel, layerUI);
XxxLayerUI是开发者自己的子类。
接下来把layer对象(包含了被装饰对象和LayerUI对象)添加到指定容器即可。
Java 7 为Frame提供了如下两个方法:
- setShape(Shape shape):设置窗口的形状,可以将窗口设置成任意不规则的形状
- setOpacity(float opacity):设置窗口透明度,可以将容器设置成半透明的。
使用 JProgressBar 可以非常方便地创建进度条,步骤如下:
<1> 创建一个 JProgressBar 对象,
<2> 调用方法设置属性
<3> 程序中工作进度改变时,调用 setValue()方法。
当进度条的完成进度改变时,可以 调用 如下两个方法
- double getPercentComplete():百分比
- String getString():进度字符串的当前值
可以用一个计时器来不断取得目标任务的完成情况,并根据其完成情况来修改进度条的value属性。
Swing 组件大都将外观显示和内部数据分离,JProgressBar有一个用于保存其状态数据的Model对象,这个对象由BoundedRangeModel对象表示。当我们调用 JProgressBar 对象的 setValue()方法时,实际上是设置 BoundedRangeModel 对象的 value 属性。
如果 BoundedRangeModel 对象的 minimum 和 maximun 属性被修改,它所对应的 JProgressBar 对象的这两个属性也会被修改。
程序监听 JProgressBar 完成比例的变化,也是通过为 BoundedRangeModel 提供监听器来实现的。
- addChangeListener(ChangeListener x):用于监听 JProgressBar 完成比例的变化,每当 JProgressBar 的value 属性变化时,系统都会触发 ChangeListener 监听器的 stateChanged() 方法。
ProgressMonitor 用法与 JProgressBar 基本相似,只是 ProgressMonitor 可以直接创建一个进度对话框。
JSlider 用法与 JProgressBar 非常相似。 使用 JSlider 可以创建一个滑动条。
两者区别如下:
- JSlider 不是采用填充颜色的方式来表示该组件的当前值,而是采用滑块的位置来表示该组件的当前值
- JSlider 允许用户手动改变滑动条的当前值
- JSlider 允许为滑动条指定刻度值,这系列的刻度值即可以是连续的数字,也可以是自定义的刻度值,甚至可以是图标
使用 JSlider 创建滑动条的步骤如下:
<1> 使用 JSlider 的构造器创建一个 JSlider 对象
<2> 调用 JSlider 的方法设置滑动条的外观样式
<3> 如果程序需要在用户拖动滑块时做出相应处理,则应用addChangeListener()方法为该 JSlider 对象添加ChangeListener事件监听器。
<4> 将 JSlider 对象添加到其他容器中显示出来
JSlider 也使用 BoundedRangeModel 作为保存其状态数据的 Model 对象,程序可以直接修改 Model 对象来改变滑动条的状态,但大部分时候程序无须使用该 Model 对象。 JSlider 也提供了 addChangeListener()方法来为滑动条添加监听器,无须像 JProgressBar 那样监听它所对应的 Model 对象。
JSpinner 组件是一个带有两个小箭头的文本框,这个文本框只能接收满足要求的数据,用户既可以通过两个小箭头调整该微调控制器的值,也可以直接在文本框内输入内容作为该微调控制器的值。当用户在该文本框内输入时,如果输入的内容不满足要求,系统将会拒绝用户输入。
JSpinner 常常需要和 SpinnerModel 结合使用,其中 JSpinner 组件控制该组件的外观表现,而 SpinnerModel 则控制该组件的内部状态数据,详见API
JList 和 JComboBox 都极其相似,它们都有一个列表框,只是 JComboBox 的列表框需要以下拉方式显示出来;它们都可以调用 setRenderer()方法来改变列表项的表现形式。甚至维护这两个组件的 Model 都是相似的,JList 使用 ListModel, JComboBox 使用 ComboBoxModel, 而 ComboBoxModel 是 ListModel 的子类
如果仅希望创建一个简单的列表框(包括 JList 和 JComboBox),则直接使用它们的构造器即可。步骤如下:
<1> 调用 JList 或者 JComboBox 构造器
<2> 调用相应方法来设置列表框的外观行为
<3> 有需要则添加监听器。JList 使用addListSelectionListener()方法 ,而 JComboBox 采用addItemListener()方法添加监听器
JList 和 JComboBox 都只负责组件外观显示,而组件底层的状态数据维护由对应的 Model 负责。JList 对应的Model使用 ListModel 接口, JComboBox 对应的Model使用 ComboBoxModel 接口
ListModel 接口不强制存储所有的列表项,ComboBoxModel 是 ListModel 的子类,也一样不强制存储。
Java 7 为 JComboBox、 List、 ListModel 都增加了泛型支持,它们的方法都可使用泛型作为参数或返回值的类型
JComboBox 类提供了几个方法来增加、插入和删除列表项。但 JList 并没有提供这些类似的方法。如果需要创建一个可以增加、删除列表项的 JList 对象,则应该在创建JList时使用 DefaultListModel 作为构造参数, 用 DefaultListModel 作为 JList 的 Model。
DefaultListModel 和 DefaultComboBoxModel 是两个强制保存所有项的 Model 类
JList 和 JComboBox 还可以支持图标列表项,可以在创建 JList 和 JComboBox 时传入图标数组。
如果希望列表项是更复杂的组件,如像QQ程序那样每个列表项既有图标,也有字符串,那么可以通过调用 JList 或 JComboBox 的 setCellRenderer(ListCellRenderer cr) 方法来实现,该方法需要接收一个 ListCellRenderer 对象,该对象代表一个列表项绘制器。
ListCellRenderer 是一个接口,包含一个方法 getListCellRendererComponent(),返回一个 Component组件,该组件就代表了一个 JList 或 JComboBox 的每个列表项。
ListCellRenderer 只是一个接口,它并未强制指定列表项绘制器属于哪种组件,因此可以采用扩展任何组件的方式来实现 ListCellRenderer 接口。通常采用其他容器(如 JPanel)的方式来实现列表项绘制器,实现列表项绘制器时可以通过重写paintComponent()方法来改变单元格的外观行为。
如目录树
Swing 使用 JTree 对象来创建一棵树(实际上, JTree 根节点),JTree树中节点可以使用 TreePath 来标识,该对象封闭了当前节点及其所有的父节点。必须指出,节点及其所有的父节点才能唯一地标识一个节点;也可以使用行数来标识。
当一个节点具有子节点时,它有两种状态:展开状态(其子节点可见),折叠状态(子节点不可见)
如果某个子节点是可见的,则该节点的父节点都必须处于展开状态。
可以直接使用JTree的构造器创建JTree的对象。其中一个构造器需要显式传入一个 TreeModel 对象,Swing为TreeModel提供了一个 DefaultTreeModel 实现类,我们可以先创建 DefaultTreeModel 对象,然后利用它创建JTree,但通过 DefaultTreeModel 的API文档会发现,创建 DefaultTreeModel 对象依然需要传入根节点,所以直接通过根节点创建JTree更加简洁。
为了利用根节点来创建JTree,程序需要创建一个TreeNode对象。TreeNode是一个接口,该接口有一个MutableTreeNode子接口,Swing为该接口提供了默认的实现类:DefaultMutableTreeNode来为树创建节点,并通过DefaultMutableTreeNode提供的add()方法建立各节点之间的父子关系,然后调用JTree的Jtree(TreeNode root)构造器来创建一棵树。
DefaultMutableTreeNode是TreeModel的默认实现类,当程序通过TreeNode类创建JTree时,其状态数据实际上由DefaultTreeModel对象维护,因为创建JTree时传入的TreeNode对象,实际上传给了DefaultTreeModel对象,并使用该DefaultTreeModel对象来创建JTree对象。
当我们把Tree对象添加到其他容器中后,JTree就会在该容器中绘制出一棵Swing树。
……(略过不看,详见《疯狂Java讲义》) 树可以做QQ的好友列表。
《疯狂Java讲义》P521
暂时不看。
《疯狂Java讲义》P575
本章所介绍的 Annotation ,其实是代码里的特殊标记,可以在编译、类加载、运行时被读取,并执行相应处理。其信息被存储在 Annotation 的“name=value”对中。
Annotation 是一个接口,程序可以通过反射来获取指定程序元素的 Annotation对象 ,然后通过 Annotation 对象来取得注释里的元数据。
本章使用Annotation的地方,有的Annotation指的是java.lang.Annotation接口,有的指的是注释本身。
Annotation能被用来为程序元素(类、方法、成员变量等)设置元数据(MetaData)。Annotation不影响程序的执行。如果希望让程序中的Annotation在运行时起一定的作用,只有通过某种配套的工具对Annotation中的信息进行访问和处理,访问和处理Annotation的工具统称API(Annotation Processing Tool)。
使用Annotation时要在其前面增加@号,并把该Annotation当成一个修饰符使用,用于修饰它支持的程序元素。
4个基本的Annotation如下:
- @Override
- @Deprecated
- @SuppressWarnings
- @SafeVarargs
SafeVarargs 是Java 7 新增的。
@Override 用来指定方法覆载,它可以强制一个子类必须覆盖父类的方法。
@Override 的作用是告诉编译器检查这个方法,保证父类要包含一个被该方法重写的方法,否则就会编译出错。这样可以避免一些低级错误。
@Deprecated 用于表示某个程序元素(类、方法等)已过时,当其他程序使用已过时的类、方法时,编译器将给出警告。
@SuppressWarnings 指示被该 Annotation 修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告。 @SuppressWarnings 会一直作用于该元素的所有子元素。
当使用@SuppressWarnings Annotation 关闭编译器警告时,一定要在括号里使用 name=value 的形式为该 Annotation 的成员变量设置值。详见下文介绍
当把一个不带泛型的对象赋给一个带泛型的变量时,往往会发生堆污染(Heap pollution)。
对于形参个数可变的方法,void faultyMethod(List<String>... listStrArray)
个数可变的形参相当于数组,但Java又不支持泛型数组,因为程序只能把List... 当成List[]处理。这时再把它赋给带泛型的变量,也会发生堆污染。
如果不希望看到堆污染警告,可以用如下三种方式:
- 使用@SafeVarargs 修饰引发该警告的方法或构造器
- 使用@SuppressWarnings("unchecked") 修饰。
- 编译时使用-Xlint:varargs 选项。
JDK 除了在java.lang 下提供了4个基本的 Annotation 之外,还在 java.lang.annotation 包下提供了4个 Meta Annotation(元 Annotation),这4个元 Annotation 都用于修饰其他的 Annotation 定义。
@Rentention 只能用于修饰一个 Annotation 定义,用于指定被修饰的 Annotation 可以保留多长时间。@Rentention 包含一个 RetentionPolicy 类型的 value 成员变量,所以使用@Rentention 时必须为该 value 成员变量指定值。
value 成员变量的值只能是如下3个。
- RetentionPolicy.CLASS:编译器将把Annotation 记录在 class 文件中。当运行 Java 程序时,JVM 不再保留 Annotation。这是默认值。
- RetentionPolicy.RUNTIME:编译器将把 Annotation 记录在 class 文件中。当运行Java程序时,JVM也会保留 Annotation ,程序可以通过反射获取该Annotation信息。
- RetentionPolicy.SOURCE:Annotation 只保留在源代码中,编译器直接丢弃这种 Annotation。
如果需要通过反射获取注释信息,就需要使用 value 属性值为 RetentionPolicy.RUNTIME 的@Retention。使用@Retention 元数据 Annotation 可采用如下代码为 value 指定值。
//定义下面的Testable Annotation 保留到运行时
@Retention(value = RetentionPolicy.RUNTIME)
public @interface Testable{}
//也可采用如下代码来为value指定值
//定义下面的Testable Annotation将被编译器直接丢弃
@Retention(RetentionPolicy.SOURCE)
public @interface Testable{}
上面代码中使用@Retention 元数据 Annotation时,并未通过 value=RententionPolicy.SOURCE的方式来为该成员变量指定值,这是因为当Annotation的成员变量名为value时,程序中可以直接在Annotation后的括号里指定该成员变量的值,无须使用name=value的形式。
@Target也只能修饰一个Annotation定义,它用于指定被修饰的Annotation能用于修饰哪些程序单元。@Target元Annotation也包含一个名为value的成员变量,该成员变量的值详见API。
与使用@Retention类似的是,使用@Target也可以直接在括号里指定value值,而无须使用name=value的形式。
如下代码指定@ActionListenrFor Annotation 只能修饰成员变量
@Target(ElementType.FIELD)
public @interface ActionListenerFor{}
@Documented用于指定被该元Annotation修饰的Annotation类将被javadoc工具提取成文档,如果定义Annotation类时使用了@Documented修饰,则所有使用该Annotation修饰的程序元素的API文档中将会包含该 Annotation说明。
下面代码定义了一个Testable Annotation,程序使用@Documented来修饰@Testable Annotation定义,所以该 Annotation将被javadoc工具所提取。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
//定义Testable Annotation将被javadoc工具提取
@Documented
public @interface Testable{}
下面代码定义了一个MyTest类,该类中的info()方法使用了@Testable修饰。
public class MyTest{
//使用@Testable修饰info方法
@Testable
public void info(){
//方法体
}
}
指定被它修饰的 Annotation 将具有继承性——如果某个类使用了@Annotation(定义时使用@Inherited修饰)修饰,则其子类将自动被@Annotation修饰。
略过不看。
Java的IO通过java.io包下的类和接口来支持。IO流有两种:输入、输出。每种输入、输出流又可分为字节流和字符流。
Java的IO流使用了一种装饰器设计模式,它将IO流分成底层节点流和上层处理流,其中节点流用于和底层的物理存储节点直接关联——程序把不同的物理节点流包装成统一的处理流,从而允许程序使用统一的输入、输出代码来读取不同的物理存储节点的资源。
Java 7 在java.nio及其子包下提供了一系列全新的API,是对原有IO的升级,因此也被称为IO2.
File类是java.io包下代表与平台无关的文件和目录,File能新建、删除、重命名文件和目录,但是不能访问文件内容本身。若要访问文件内容本身,需要用输入、输出流。
File类可以使用文件路径字符串来创建File实例。默认情况下,系统总是根据用户的工作路径来解释相对路径,这个路径由系统属性“user.dir”指定,通常也就是运行Java虚拟机时所在的路径。
File类的常用方法:
<1>访问文件名相关方法
- String getName():返回由此抽象路径名表示的文件或目录的名称。如果路径名名称序列为空,则返回空字符串。
- String getPath():返回路径名
- File getAbsoluteFile():返回此File的绝对路径名形式。
- String getAbsolutePath():返回此File的绝对路径名。
- String getParent():返回File的父目录名;如果此路径名没有指定父目录,则返回null
- boolean renameTo(File dest):重命名File对应的文件或目录
<2>文件检测相关的方法
- boolean exists():判断File对应的文件或目录是否存在。
- boolean canWrite():判断File对应的文件和目录是否可写
- boolean canRead():判断File对应的文件和目录是否可读
- boolean isFile():判断File对应的是否是文件
- boolean isDirectory():判断File对应的是否是目录
- boolean isAbsolute():判断File对应的文件或目录是否绝对路径。
<3>获取常规文件信息
- long lastModified():返回文件最后一次被修改的时间。
- long length():返回文件内容的长度
<4>文件操作的相关方法
- boolean createNewFile() throws IOException:当此File对应的文件不存在时,该方法将新建一个File对象所指定的新文件,创建成功才返回 true
- boolean delete():删除File对应的文件或目录
- static File createTempFile(String prefix, String suffix) throws IOException:在默认临时文件目录中创建一个临时的空文件,使用给定前缀、系统生成的随机数和给定后缀作为文件名。prefix至少三字符长,若suffix为null,则使用默认后缀.tmp。调用此方法等同于调用 createTempFile(prefix, suffix, null)。
- static File createTempFile(String prefix, String suffix, File directory) throws IOException:在指定目录中创建一个临时的空文件,使用给定的前缀、系统生成的随机数和后缀字符串作为文件名。
- void deleteOnExit():注册一个删除钩子,指定当Java虚拟机终止时,File对应的文件或目录
<5>目录操作相关的方法
- boolean mkdir():创建File对象指定的目录。创建成功则返回 true
- String[] list():返回File对象表示的目录中的文件名和目录名。如果目录为空,那么返回数组也将为空。如果此抽象路径名不表示一个目录,或者发生 I/O 错误,则返回 null。
- File[] listFiles():返回File表示的目录中的文件。
- static File[] listRoots():列出系统所有的根路径。
package King.exercise;
import java.io.File;
import java.io.IOException;
public class FileTest{
public static void main(String[] args)
throws IOException{
// 当前路径创建一个File对象
File file = new File(".");
System.out.println("直接获取文件名:" + file.getName());
System.out.println("获取相对路径的父路径:" + file.getParent());
System.out.println("获取绝对路径:" + file.getAbsoluteFile());
System.out.println("获取上一级路径:" + file.getAbsoluteFile().getParent());
//在当前路径下新建一个临时文件
File tmpFile = File.createTempFile("aaa", ".txt", file);
//JVM退出时删除该文件
tmpFile.deleteOnExit();
//以系统当前时间作为新文件名创建新文件
File newFile = new File(System.currentTimeMillis() + "");
System.out.println("newFile对象是否存在:" + newFile.exists());
//以指定newFile对象来创建一个文件
newFile.createNewFile();
System.out.println("createNewFile()后,newFile对象是否存在:" + newFile.exists());
System.out.println("newFile对象的路径:" + newFile.getAbsoluteFile());
System.out.println("newFile对象的路径字符串:" + newFile.getAbsolutePath());
//以newFile创建目录,因为newFile已经存在,返回false
System.out.println("新建newFile目录结果:" + newFile.mkdir());
// newFile.mkdir();
//列出当前路径下的所有文件和路径
String[] fileList = file.list();
System.out.println("=====当前路径下所有文件和路径如下=====");
for(String fileName:fileList){
System.out.println(fileName);
}
//列出所有磁盘根路径
File[] roots = File.listRoots();
System.out.println("=====系统所有根路径如下:=====");
for(File root:roots){
System.out.println(root);
}
}
}
当使用相对路径的File对象来获取父路径时可能引起错误,因为该方法返回将File对象所对应的目录名、文件名里最后一个目录名、子文件名删除后的结果。
运行结果:
直接获取文件名:.
获取相对路径的父路径:null
获取绝对路径:F:\EclipseProject\Test.
获取上一级路径:F:\EclipseProject\Test
newFile对象是否存在:false
createNewFile()后,newFile对象是否存在:true
newFile对象的路径:F:\EclipseProject\Test\1418278551421
newFile对象的路径字符串:F:\EclipseProject\Test\1418278551421
新建newFile目录结果:false
=====当前路径下所有文件和路径如下=====
.classpath
.project
1418278127953
1418278551421
aaa7224950151126864266.txt
bin
src
=====系统所有根路径如下:=====
C:\
D:\
E:\
F:\
Windows的路径分隔符使用反斜线(\),而Java程序中的反斜线表示转义字符,所以如果需要在Windows的下包括反斜线,则应该使用两条反斜线,或者直接使用斜线(/)
File类的list()方法中可以接收一个FilenameFilter参数,只列出符合条件的文件。FilenameFilter接口和javax.swing.filechooser包下的FileFilter抽象类的功能非常相似。
FilenameFilter接口里包含了一个accept(File dir, String name)方法,该方法将依次对指定File的所有子目录或者文件进行迭代,如果该方法返回true,则list()方法会列出该子目录或文件
//实现自己的FilenameFilter实现类
class MyFilenameFilter implements FilenameFilter {
public boolean accept(File dir, String name){
//如果文件名以.java结尾,或者文件对应一个路径,则返回true
return name.endsWith(".java") || new File(name).isDirectory();
}
}
这种用法是一个典型的Command设计模式,因为File的list()方法需要一个代码块作为判断规则,但当时的JDK版本不支持直接向方法传入代码块,所以Java使用了FilenameFilter的accept()方法来封装该代码块。
Java中把不同的输入/输出源(键盘、文件、网络连接等)抽象表述为“流”(stream),通过流的方式允许Java程序使用相同的方式来访问不同的输入/输出源。stream是从起源(source)到接收(sink)的有序数据。
所有传统的流类型都放在java.io包中。
<1>流向:输入流和输出流
- 输入流:只能读不能写,InputStream和Reader作为基类
- 输出流:只能写不能读,OutputStream和Writer作为基类
这里的输入、输出都是从程序运行所在内存的角度来划分的。
如果从Server向Client传输数据,那么Server的内存负责将数据输出到网络,使用输出流;Client的内存负责从网络里读取数据,使用输入流。
<2>操作的数据单元:字节流和字符流
- 字节流:8位的字节,InputStream和OutputStream作为基类
- 字符流:16位的字符,Reader和Writer作为基类
<3>流的角色:节点流和处理流
- 节点流:可以从/向一个特定的IO设备(如磁盘、网络)读/写数据的流。也称低级流(Low Level Stream)。连接到实际的数据源
- 处理流:对一个已存在的流进行连接或封装,通过封装后的流来实现数据读/写功能。也称高级流。不会直接连接到实际的数据源
只要使用相同的处理流,程序就可以采用完全相同的输入/输出代码来访问不同的数据源,随着处理流所包装节点流的变化,程序实际访问的数据源也相应地发生变化。又称包装流。这是一种典型的装饰器设计模式。
Java把所有设备里的有序数据抽象成流模型。
Java的IO流的40多个类都是从如下4个抽象基类派生的:InputStream/Reader,OutputStream/Writer。
处理流的功能主要体现在两个方面:
- 性能的提高:主要以增加缓冲的方式来提高输入/输出的效率
- 操作的便捷:处理流可能提供了一系列便捷的方法来一次输入/输出大指的内容,而不是输入/输出一个或多个数据单元。
处理流可以“嫁接”在任何已存在的流的基础之上。
InputStream和Reader是所有输入流的抽象基类。它们的方法是所有输入流都可用的方法。
InputStream包含如下方法:
- int available():返回此输入流下一个方法调用可以不受阻塞地从此输入流读取(或跳过)的估计字节数。
- void close():关闭此输入流并释放与该流关联的所有系统资源。
- void mark(int readlimit):在此输入流中标记当前的位置。
- boolean markSupported():测试此输入流是否支持 mark 和 reset 方法。
- abstract int read():从输入流中读取数据的下一个字节。返回所读取的字节数
- int read(byte[] b):从输入流中读取b.length个字节,并将其存储在缓冲区数组 b 中。 返回实际读取的字节数
- int read(byte[] b, int off, int len):从输入流中读取最多 len 个字节,存入 byte 数组(从off)位置开始。返回实际读取的字节数。
- void reset():将此流重新定位到最后一次对此输入流调用 mark 方法时的位置。
- long skip(long n):跳过和丢弃此输入流中数据的 n 个字节。
Reader里包含如下方法:
- abstract void close():关闭该流并释放与之关联的所有资源。
- void mark(int readAheadLimit):标记流中的当前位置。
- boolean markSupported():判断此流是否支持 mark() 操作。
- int read():读取单个字符,转换成int后返回。
- int read(char[] cbuf):将字符读入数组,最多cbuf.length个。返回实际读取的个数。
- abstract int read(char[] cbuf, int off, int len):将字符读入数组的某一部分。
- int read(CharBuffer target):试图将字符读入指定的字符缓冲区。
- boolean ready():判断是否准备读取此流。
- void reset():重置该流。
- long skip(long n):跳过字符。
InputStr和Reader分别有一个用于读取文件的输入流:FileInputStream和FileReader,它们都是节点流——直接和指定文件关联。
下面用FileInputStream读取文件。
import java.io.FileInputStream;
import java.io.IOException;
public class FileInputStreamTest {
public static void main(String[] args) throws IOException{
//创建字节输入流
FileInputStream fis = new FileInputStream("FileInputStreamTest.java");
byte[] bbuf = new byte[1024]; //用于读取字节
int hasRead = 0; //用于保存实际读取的字节数
while((hasRead = fis.read(bbuf)) > 0){
System.out.print(new String(bbuf, 0, hasRead));
}
fis.close(); //关闭文件输入流,放在finally块里更安全
}
}
如果创建较小的字节数组读取文件,程序运行时在输出中文就可能出现乱码——如果文件保存时采用GBK编码,在这种方式下,每个中文字符占2字节,如果read()方法读取时只读到了半个中文字符,就会导致乱码。
程序里打开的文件IO资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭IO资源。 Java 7 改写了所有的IO资源类,它们都实现了AutoCloseable接口,因此都可通过自动关闭资源的try语句来关闭IO流。
下面程序使用FileReader来读取文件本身。
import java.io.FileReader;
import java.io.IOException;
public class FileReaderTest {
public static void main(String[] args) throws IOException{
try(FileReader fr = new FileReader("FileReaderTest.java")){
char[] cbuf = new char[10]; //用于读取
int hasRead = 0; //保存实际读取的字符数
while((hasRead = fr.read(cbuf)) > 0){
System.out.print(new String(cbuf, 0, hasRead));
}
}
catch (IOException ex){
ex.printStackTrace();
}
}
}
OutputStream和Writer也非常相似。
OutputStream:
- void close():关闭此输出流并释放与此流有关的所有系统资源。
- void flush():刷新此输出流并强制写出所有缓冲的输出字节。
- void write(byte[] b):将 b.length 个字节从指定的 byte 数组写入此输出流。
- void write(byte[] b, int off, int len):将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此输出流。
- abstract void write(int b):将指定的字节写入此输出流。
由于字符流直接以字符为操作单位,所以Writer可以用字符串来代替字符数组。
- Writer append(char c):将指定字符添加到此 writer。
- Writer append(CharSequence csq):将指定字符序列添加到此 writer。
- Writer append(CharSequence csq, int start, int end):将指定字符序列的子序列添加到此 writer.Appendable。
- abstract void close():关闭此流,但要先刷新它。
- abstract void flush():刷新该流的缓冲。
- void write(char[] cbuf):写入字符数组。
- abstract void write(char[] cbuf, int off, int len):写入字符数组的某一部分。
- void write(int c):写入单个字符。
- void write(String str):写入字符串。
- void write(String str, int off, int len):写入字符串的某一部分。
下面程序使用FileInputStream来执行输入,并使用FileOutputStream来执行输出,用以实现复制FileOutputStreamTest.java文件的功能。
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileOutputStreamTest {
public static void main(String[] args) {
try{
FileInputStream fis = new FileInputStream("./src/King/exercise/FileOutputStreamTest.java");
FileOutputStream fos = new FileOutputStream("./src/King/exercise/newFile.txt");
byte[] bbuf = new byte[32];
int hasRead = 0;
while((hasRead = fis.read(bbuf)) >0){
fos.write(bbuf, 0, hasRead);
}
fis.close();
fos.close();
}
catch (IOException ioe){
ioe.printStackTrace();
}
}
}
输出流在执行close()方法之前,自动执行输出流的flush()方法,从而将输出流缓冲区中的数据flush到物理节点里。Java的很多输出流默认都提供了缓冲功能。
用Writer输出字符串
public class FileWriterTest{
public static void main(String[] args){
try{
FileWriter fw = new FileWriter("FileWriterTest.txt");
fw.write("锦瑟 - 李商隐\r\n");
fw.write("锦瑟无端五十弦,一弦一柱思华年。\r\n");
fw.write("庄生晓梦迷蝴蝶,望帝春心托杜鹃。\r\n");
fw.write("沧海月明珠有泪,蓝田日暖玉生烟。\r\n");
fw.write("此情可待成追忆,只是当时已惘然。\r\n");
fw.close();
}
catch (IOException ioe){
ioe.printStackTrace();
}
}
}
上面程序字符串最后的\r\n是Windows平台的换行符
使用处理流包装节点流,程序通过处理流来执行输入/输出功能,让节点流与底层的I/O设备、文件交互。
所有节点流都是直接以物理IO节点作为构造器参数的,而处理流则相反。
下面程序使用PrintStream处理流来包装OutputStream,使用处理流后的输出流在输出时将更加方便。
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
public class PrintStreamTest {
public static void main(String[] args) {
try{
FileOutputStream fos = new FileOutputStream("./src/King/exercise/test.txt");
PrintStream ps = new PrintStream(fos);
ps.println("普通字符串"); //使用PrintStream执行输出
ps.println(new PrintStreamTest()); //直接使用PrintStream输出对象:对象名+Hash地址
ps.close();
fos.close();
}
catch (IOException ioe){
ioe.printStackTrace();
}
}
}
前面学习中一直使用的System.out的类型也是PrintStream,PrintStream的输出功能非常强大,通常如果要输出文本内容,都应该将输出包装成PrintStream后进行输出
在使用处理流包装了底层节点流之后,在关闭输入/输出流资源时,只要关闭最上层处理流即可,此时系统会自动关闭被该处理流包装的节点流。
常用流分类:
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
---|---|---|---|---|
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
访问管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
访问字符串 | StringReader | StringWriter | ||
缓冲流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
转换流 | InputStreamReader | OutputStreamWriter | ||
对象流 | ObjectInputStream | ObjectOutputStream | ||
抽象基类 | FilterInputStream | FilterOutputStream | FilterReader | FilterWriter |
打印流 | PrintStream | PrintWriter | ||
推回输入流 | PushbackInputStream | PushbackReader | ||
特殊流 | DataInputStream | DataOutputStream |
表中四个访问开头的为节点流,必须与指定的物理节点相关联。表中仅列出java.io包下的流,还有些诸如AudioInputStream、CipherInputStream、DeflaterInputStream、ZipInputStream等具有访问音频文件、加密/解密、压缩/解压等功能的字节流,位于JDK的其他包下。
文本内容用字符流,二进制内容用字节流。
实际上文本文件只是二进制文件的一种特例,当二进制文件里的内容恰好能被正常解析成字符时,该文件即文本文件。打开文件若用错字符集则为乱码。Windwos下简体中文默认为GBK字符集,Linux下默认用UTF-8字符集。
访问管道的流用于实现进程之间的通信功能。
缓冲流增加了缓冲功能,可以提高输入、输出的效率,增加缓冲功能后需要使用flush()才可以将缓冲区的内容写入实际的物理节点。
下面示范使用字符串作为物理节点的字符输入/输出流的用法
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
public class StringNodeTest{
public static void main(String[] args){
String src = "从明天起,做一个幸福的人\n"
+ "喂马,劈柴,周游世界\n";
char[] buffer = new char[32];
int hasRead = 0;
try{
StringReader sr = new StringReader(src);
while((hasRead = sr.read(buffer)) > 0){
System.out.print(new String(buffer, 0, hasRead));
}
sr.close();
}
catch (IOException ioe){
ioe.printStackTrace();
}
try{
//创建StringWriter时,实际以一个StringBuffer作为输出节点
//下面指定的20就是StringBuffer的初始长度
StringWriter sw = new StringWriter();
sw.write("有一个美丽的新世界,\n");
sw.write("她在远方等我,\n");
System.out.println(sw.toString());
sw.close();
}
catch (IOException ex){
ex.printStackTrace();
}
}
}
InputStreamReader, OutputStreamWriter
转换流用于实现将字节流转换成字符流。
Java使用System.in代表标准输入,即键盘输入,但这个标准输入流是InputStream类的实例,使用不太方便,而且键盘输入内容都是文本内容,所以可以使用InputStreamReader将其转换成字符输入流。普通的Reader读取输入内容时依然不太方便,我们可以将普通的Reader再次包装成BufferedReader,利用BufferedReader的readLine()方法可以一次读取一行内容。以换行符为标志,如果没有读到换行符,则程序阻塞。
//try catch略过
InputStreamReader reader = new InputStreamReader(System.in);
BufferedReader br = new BufferedReader(reader);
String buffer = null;
while((buffer = br.readLine()) != null){
if(buffer.equals("exit")){
System.exit(1);
}
System.out.println("输入内容为:" + buffer);
}
PushbackInputStream 和 PushbackReader 。
提供了三个unread方法,具体见API。 用于将字节、字符推回缓冲区,从而允许重复读取刚刚读取的内容。
这两个推回输入流都带有一个推回缓冲区,当程序调用这两个推回输入流的unread()方法时,系统将会把指定数组的内容推回到该缓冲区里,而推回输入流每次调用read()方法时问题先从推回缓冲区读取,只有完全读取了推回缓冲区的内容后,但还没有装满read()所需的数组时才会从原输入流中读取。
在创建这两个转换流时需指定推回缓冲区的大小,默认的推回缓冲区的长度为1.如果程序中推回到推回缓冲区的内容超出了推回缓冲区的大小,将会引发Pushback buffer overflow的IOException异常。
下面程序试图找出程序中的“new PushbackReader”字符串,当找到该字符串后,程序只是打印出目标字符串之前的内容
import java.io.FileReader;
import java.io.IOException;
import java.io.PushbackReader;
public class PushbackTest {
public static void main(String[] args) {
try{
//创建一个PushbackReader对象,指定推回缓冲区的长度为64
PushbackReader pr = new PushbackReader(new FileReader("./src/King/exercise/PushbackTest.java"), 64);
char[] buf = new char[32];
String lastContent = "";//保存上次读取的字符串内容
int hasRead = 0;
while((hasRead = pr.read(buf))>0){
//将读取的内容转换成字符串
String content = new String(buf, 0, hasRead);
int targetIndex = 0;
//将上次读取的字符串和本次读取的字符串拼起来,查看是否包含目标字符串
// 这样可以避免目标字符串正好被分割成两半的情况
if ((targetIndex = (lastContent + content).indexOf("new PushbackReader"))>0){
//将本次内容和上次内容一起推回缓冲区
pr.unread((lastContent + content).toCharArray());
//指定读取前面len个字符
int len = targetIndex > 32 ? 32 : targetIndex;
pr.read(buf, 0, len);
System.out.print(new String(buf, 0, len));
System.exit(0);
pr.close();
}
else{
System.out.print(lastContent);
lastContent = content;
}
}
}
catch(IOException ioe){
ioe.printStackTrace();
}
}
}
Java 的标准输入/输出分别通过System.in 和 System.out 来代表,在默认情况下它们分别代表键盘和显示器。
System类里提供了3个重定向标准输出/输出的方法:
- static void setErr(PrintStream err):重定向“标准”错误输出流。
- static void setIn(InputStream in):重定向“标准”输入流。
- static void setOut(PrintStream out):重定向“标准”输出流。
//一次性创建PrintStream输出流
PrintStream ps = new PrintStream(new FileOutputStream("out.txt"));
//将标准输出重定向到ps输出流
System.setOut(ps);
System.out.println("String");
FileInputStream fis = new FileInputStream("RedirectIn.java");
//将标准输入重定向到fis输入流
System.setIn(fis);
//使用System.in创建Scanner对象,用于获取标准输入
Scanner sc = new Scanner(System.in);
//只把回车作为分隔符
sc.useDelimiter("\n");
Runtime对象的exec()方法可以运行平台上的其他程序,该方法产生一个Process对象,Process对象代表由该Java程序启动的子进程。
Process类提供了如下3个方法用于让程序和其子进程进行通信:
- abstract InputStream getErrorStream():获取子进程的错误流。
-abstract InputStream getInputStream():获取子进程的输入流。
-abstract OutputStream getOutputStream():获取子进程的输出流。
如果子进程读取程序中的数据,也就是让程序把数据输出到子进程中(就像把数据输出到文件中一样,只是现在由子进程节点代替了文件节点),应该用输出流。
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class ReadFromProcess {
public static void main(String[] args) throws IOException {
//运行javac命令,返回运行命令的子进程
Process p = Runtime.getRuntime().exec("javac");
try{
//以p进程的错误输入流创建BufferedReader对象
//这个错误流对本程序是输入流,对p进程则是输出流
//数据从进程流向本程序,从本程序的角度,是输入流
BufferedReader br = new BufferedReader(new InputStreamReader(p.getErrorStream()));
String buff = null;
while((buff = br.readLine()) != null){
System.out.println(buff);
}
bf.close();
}
catch (IOException ioe){
ioe.printStackTrace();
}
}
}
下面程序实现了在Java程序中启动Java虚拟机运行另一个Java程序,并向另一个Java程序中输入数据。
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Scanner;
public class WriteToProcess {
public static void main(String[] args) throws IOException {
Process p = Runtime.getRuntime().exec("java ReadStandard");
//以p进程的输出流创建PrintStream对象
//这个输出流对本程序是输出流,对p进程则是输入流
PrintStream ps = new PrintStream(p.getOutputStream());
//向ReadStandard程序写入内容,这些内容将被ReadStandard读取
ps.println("String");
ps.println(new WriteToProcess());
ps.close();
}
}
//定义一个ReadStandard类,该类可以接收标准输入,将将标准输入写入out.txt文件
class ReadStandard{
public static void main(String[] args){
try{
Scanner sc = new Scanner(System.in);
PrintStream ps = new PrintStream(new FileOutputStream("out.txt"));
sc.useDelimiter("\n");
while(sc.hasNext()){
ps.println("键盘输入的内容是:" + sc.next());
}
ps.close();
}
catch (IOException e){
e.printStackTrace();
}
}
}
RandomAccessFile 是 Java输入/输出流体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问文件内容,既可以读取文件内容,也可以向文件输出数据。与普通的输入/输出流不同的是,RandomAccesFile 支持“随机访问”的方式,程序可以直接跳转到文件的任意地方来读写数据。所以它可以向已存在的文件后追加内容。
RandomAccessFile 提供了如下两个方法来操作文件记录指针
- long getFilePointer():返回文件记录指针的当前位置
- void seek(long pos):将文件记录指针定位到pos位置
RandomAccessFile既可以读也可以写,所以它包含了读、写方法,用法与InputStream和OutputStream完全一样。
题外话:
随机访问:Random Access,确切地说是任意访问
内存:RAM, Ramdom Access Memory。随文解意,RAM是可以自由访问任意存储点的存储器(与磁盘、磁带等需要寻道、倒带才可访问指定存储点等存储器相区分)。
RandomAccessFile有两个构造器,详见API。
访问指定的中间部分的数据:
RandomAccessFile raf = new RandomAccessFile("RandomAccessFileTest.java", "r");
//获取 RandomAccessFile 对象文件指针的位置,初始位置是0
System.out.println("RandomAccessFile的文件指针的初始位置:" + raf.getFilePointer());
//移动raf的文件记录的位置
raf.sddk(300);
byte[] bbuf = new byte[1024];
int hasRead = 0;
while ( (hasRead = raf.read(bbuf)) > 0){
System.out.print(new String(bbuf, 0, hasRead));
}
向指定文件后追加内容:
RandomAccessFile raf = new RandomAccessFile("out.txt", "rw");
raf.seek(raf.length());
raf.write("追加的内容!\rn".getBytes());
如果要向指定位置插入内容,需要先把插入点后面的内容读入缓冲区,等把需要插入的数据写入文件后,再将缓冲区的内容追加到文件后面
//关键部分代码
File tmp = File.createTempFile("tmp", null);
tmp.deleteOnExit();
RandomAccessFile raf = new RandomAccessFile(fileName, "rw");
//创建一个临时文件来保存插入点后的数据
FileOutputStream tmpOut = new FileOutputStream(tmp);
FileInputStream tmpIn = new FileInputStream(tmp);
long pos = 45; //要插入的位置
String insertContent = "要插入的内容";
String fileName = "this.txt";
raf.seek(pos);
//下面代码将插入点后的内容读入临时文件中保存
byte[] bbuf = new byte[64];
int hasRead = 0;
while((hasRead = raf.read(bbuf)) > 0 ){
tmpOut.Write(bbuf, 0, hasRead);
}
//下面代码用于插入内容
//把文件记录指针重新定位到pos位置
raf.seek(pos);
//追加需要插入的内容
raf.write(insertContent.getBytes());
// 追加临时文件中的内容。
while((hasRead = tmpIn.read(bbuf)) > 0){
raf.write(bbuf, 0, hasRead);
}
多线程断点的网络下载工具就可通过RandomAccessFile类来实现,所有的下载工具在下载开始时都会建立两个文件:一个是与被下载文件大小相同的空文件,一个是记录文件指针的位置文件,下载工具用多条线程启动输入流来读取网络数据,并使用RandomAccessFile将从网络上读取的数据写入前面建立的空文件中,每写一些数据后,记录文件指针的文件就分别记下每个RandomAccessFile 当前的文件指针位置——网络断开后,再次开始下载时,每个RandomAccessFile都根据记录文件指针的文件中记录的位置继续向下写数据。
目标:是将对象保存到磁盘中,或允许在网络中直接传输对象。
对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。其他程序获取后可以恢复成原来的Java对象。
序列化机制使对象可以脱离程序的运行而独立存在。
对象的序列化(Serialize)针一个Java对象写入IO流中。反序列化(Dederialize)则指从IO流中恢复该Java对象。
如果想让某个对象支持序列化的机制,则它的类必须实现如下两个接口之一:
- Serializable:一个标记接口,无须实现任何方法。它只是表明该类的实例是可序列化的
- Externalizable
Java的很多类已经实现了Serializable。
所有可能在网络上传输的和保存到磁盘中的对象类都应该是可序列化的,否则程序将会出现异常。
使用Serializable实现序列化非常简单,只要让目标类实现Serializable标记接口即可,无须实现任何方法。
序列化对象的方法:
<1> 创建一个ObjectOutputStream。它是处理流,必须建立在节点流的基础上
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
<2>调用ObjectOutputStream对象的writeObject()方法输出可序列化对象
oos.writeObject(per); //per是对象
如果希望从二进制流中恢复Java对象,则需要使用反序列化,步骤如下:
<1>创建一个ObjectInputStream输入流。
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
<2>调用ObjectInputStream对象的readObject()方法读取流中的对象。该方法返回一个Object类型的Java对象,如果知道该Java对象的类型,则可以将该对象强制类型转换成其真实的类型
Person p = (Person)ois.readObject();
反序列化读取的仅仅是Java对象的数据,而不是Java类,因此采用反序列化恢复Java对象时,必须提供该Java对象所属类的class文件,否则将引发ClassNotFoundException异常。
反序列化无须通过构造器来初始化Java对象。
如果使用序列化机制向文件中写入多个Java对象,使用反序列化机制恢复对象时必须按实际写入的顺序读取。
当一个可序列化类有多个父类时(包括直接父类和间接父类),这些父类要么有无参数的构造器,要么也是可序列化的——否则反序列化时将抛出InvalidClassException异常。如果父类是不可序列化的,只是带有无参数的构造器,则该父类中定义的成员变量值不会序列化到二进制流中。
如果某个类的成员变量类型不是基本类型或String类型,而是另一个引用类型,那么这个引用类必须是可序列化的,否则拥有该类型的成员变量的类也是不可序列化的。
递归序列化:当对某个对象进行序列化时,系统会自动把该对象的所有Field依次进行序列化,如果某个Field引用到另一个对象,则被引用的对象也会被序列化,如果仍有对象引用则会序列化其他对象,这种情况被称为递归序列化。
Java序列化机制采用了一种特殊的序列化算法:
- 所有保存到磁盘中的对象都有一个序列化编号。
- 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列输出。
- 如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象。
序列化+反序列化示例
……
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Teacher.txt"));
Person per = new Person("孙悟空", 500);
Teacher t1 = new Teacher("唐僧", per);
Teacher t2 = new Teacher("菩提祖师", per);
oos.writeObject(t1);
oos.writeObject(t2);
oos.writeObject(per);
oos.writeObject(t2);
……
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Teacher.txt"));
Teacher t1 = (Teacher)ois.readObject();
Teacher t2 = (Teacher)ois.readObject();
Person p = (Person)ois.readObject();
Teacher t3 = (Teacher)ois.readObject();
这种序列化机制存在一个潜在的问题——当程序序列化一个可变对象时,只有第一次使用writeObject()方法输出时才会将该对象转换成字节序列并输出,当程序再次调用writeObject()方法时,程序只是输出前面的序列化编号,即使后面该对象的成员变量值已经被改变,改变的成员变量值也不会被输出
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("mutable.txt"));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("mutable.txt"));
Person per = new Person("孙悟空", 500);
//将per对象转换成字节序列并输出
oos.writeObject(per);
//改变per对象的name值
per.setName("猪八戒");
//系统只是输出序列化编号,所以改变后的name不会被序列化
oos.writeObject(per);
通过在成员变量前面使用transient关键字修饰,可以指定Java序列化时无须理会该成员变量。这样导致反序列化恢复Java对象时无法取得该成员变量值。
Java还提供了一种自定义序列化机制,可以让程序控制如何序列化各成员变量,甚至完全不序列化某些成员变量(与transient关键字的效果相同)。
在序列化和反序列化过程中需要特殊处理的类应该提供如下特殊签名的方法,这些特殊的方法用以实现自定义序列化。
- private void writeObject(java.io.ObjectOutputStream out) throws IOException:默认调用out.defaultWriteObject来保存Java对象的各成员变量
- private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException:默认调用in.defaultReadObject来保存Java对象的各成员变量。
- private void readObjectNoData() throws ObjectStreamException:序列化流不完整时,用来正确地初始化反序列化的对象。例如:接收方使用的反序列化类的版本不同于发送方,或者接收方版本扩展的类不是发送方版本扩展的类,或者序列化流被篡改。
示例
public class Person implements java.io.Serializable{
private String name;
private int age;
//注意此处没有提供无参数的构造器
public Person(String name, int age){
System.out.println("有参数的构造器");
this.name = name;
this.age = age;
}
……省略其他方法
private void writeObject(java.io.ObjectOutputStream out) throws IOException{
//将name反转后写入二进制流
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//将读取的字符串反转后赋给name
this.name = ((StringBuffer)in.readObject()).reverse().toString();
this.age = in.readInt();
}
}
还有一种更彻底的自定义机制,它甚至可以在序列化对象时将该对象替换成其他对象。
为序列化类提供如下特殊方法
ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException
ANY-ACCESS-MODIFIER 表示任意访问权限修饰符。
例子,在写入某对象时将该对象替换成ArrayList:
private Object writeReplace() throws ObjectStreamException{
ArrayList<Object> list = new ArrayList<Object>();
list.add(name);
list.add(age);
return list;
}
Java在序列化某个对象之前,先调用该对象的writeReplace()和writeObject()两个方法。
序列化对象里还有一个特殊的方法,可以实现保护性复制整个对象
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException
这个方法会紧接着readObject()之后被调用,其返回值会代替原来反序列化的对象,而原来readObject()反序列化的对象将会被立即丢弃。在序列化单例类、枚举类时尤其有用。
对于单例类来说,虽然把构造器设为private,但是反序列化机制在恢复Java对象时无须调用构造器来初始化Java对象,所以反序列化后的对象跟之前的并不一样。可以说是被克隆了一个。这时可以用readResolve()方法
private Object readResolve() throws ObjectStreamException{
if(value == 1) return HORIZONTAL;
}
所有的单例类、枚举类在实现序列化时都应该提供readResolve()方法。
由于readResolve()方法可以使用任意的访问控制符,父类的readResolve()方法可能被其子类继承。如果子类没有重写该方法,将会使子类反序列化时得到一个父类的对象。所以实现readResolve()方法可能有一些潜在的风险。除final类外,应尽量使用private修饰。
该方式完全由程序员决定存储和恢复对象数据。必须实现Externalizable接口,其中有两个方法:
- void readExternal(ObjectInput in):用于反序列化。它通过调用 DataInput(它是ObjectInput的父接口)的方法来恢复其基本类型(int float....),调用 ObjectInput的readObject 来恢复对象、字符串和数组。
- void writeExternal(ObjectOutput out):用于序列化。它可以通过调用 DataOutput (它是ObjectOutput的父接口)的方法来保存其基本类型,或调用 ObjectOutput 的 writeObject 方法来保存对象、字符串和数组。
采用实现Externalizable接口方式的序列化与前面介绍的自定义序列化非常相似,只是它强制自定义序列化。
public void writeExternal(java.io.ObjectOutput out) throws IOException{
out.writeObject(new StringBuffer(name).reverse());
out.writeInt(age);
}
public void readExternal(java.io.ObjectInput in) throws IOException, ClassNotFoundException{
this.name = ((StringBuffer)in.readObject()).reverse().toString();
this.age = in.readInt();
}
实现Externalizable接口序列化与反序列化性能比Serializable性能略好。
关于对象序列化,还有如下几点要注意:
- 对象的类名、成员变量都会被序列化;方法、静态成员变量、transient(瞬态)成员变量都不会被序列化。
- 保证序列化对象的成员变量类型也是可序列化的,否则需要使用transient关键字来修饰该成员变量
- 反序列化对象时必须有序列化对象的class文件。
- 读取序列化后的对象时,必须按实际写入的顺序读取
随着项目的升级,系统的class文件也会升级,如果保证两个class文件的兼容性?
Java序列化机制允许为序列化类提供一个private static final的serialVersionUID值,用于标识该Java类的序列化版本
public class Test{
private static final long serialVersionUID = 512L; //具体数值自己定义
}
最好在每个要序列化的类中加入private static final long serialVersionUID 这个成员变量。
可以通过JDK安装路径的bin目录下的serialver.exe工具来获得该类的serialVersionUID成员变量值
serialver Person //如果指定-show选项,则会启动一个图形用户界面
前面介绍的输入、输出流都是阻塞式的输入、输出,如果没有读到有效数据,就会阻塞该线程的执行。
面向流的输入/输出系统一次只能处理一个字节(底层实现都依赖于字节处理),通常效率不高。
JDK 1.4开始,Java提供了一系列改进输入/输出处理的新功能,梳称NIO,放在java.nio包以及子包下。并且对原java.io包中的很多类都以NIO为基础进行了改写,新增了满足NIO的功能。
新IO采用内存映射的方式来处理输入/输出,它将文件事文件的一段区域映射到内存中,这样就可以像访问内存一样访问文件了(这种方式模式了操作系统上的虚拟内存的概念),通过这种方式来进行输入/输出比传统的输入/输出要快得多。
新IO相关包如下:
- java.nio :Buffer相关的类
- java.nio.channels:Channel和Selector相关的类
- java.nio.charset:字符集相关的类
- java.nio.channels.api:与Channel相关的服务提供者编程接口
- java.nio.charset.spi:与字符集相关的服务提供者接口
Channel(通道)和Buffer(缓冲)是新IO中的两个核心对象,Channel是对传统的输入/输出系统的模拟,在新IO系统中所有的数据都要通过通道传输;Channel与传统的InputStream、OutputStream最大的区别在于它提供了一个map()方法,通过该map()方法可以直接将“一块数据”映射到内存中。如果说传统的输入/输出系统是面向流的处理,则新IO是面向块的处理。
Buffer可以理解成一个容器,它的本质是一个数组,发送到Channel中的所有对象以及从Channel中读取的数据都必须首先放到Buffer中。
Charset用于将Unicode字符映射成字节序列以及逆映射
Selector用于支持非阻塞式输入/输出。
Buffer是一个抽象类,其最常用的子类是ByteBuffer,它可以在底层字节数组上进行get/set操作。除此之外,对应于其他基本数据类型(boolean除外)都有相应的Buffer类:CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。
除了ByteBuffer之外,都采用相同或相似的方法来管理数据。这些Buffer类都没有提供构造器,通过使用如下方法来得到一个Buffer对象:
- static XxxBuffer allocate(int capacity):创建一个容量为capacity的XxxBuffer对象
实际使用较多的是ByteBuffer和CharBuffer,其他Buffer子类则较少用到。其中ByteBuffer类还有一个子类:MappedByteBuffer,用于表示Channel将磁盘文件的部分或全部内容映射到内存中后得到的结果,通常MappedByteBuffer对象由Channel的map()方法返回。
Buffer中有3个重要的概念:容量(capacity)、界限(limit)和位置(position)。
- 容量(capacity):该Buffer的最大数据容量,创建后不能改变
- 界限(limit):第一个不应该被读出或者写入的缓冲区位置索引。也就是说位于limit后的数据既不可被读,也不可被写。
- 位置(position):用于指明下一个可以被读出的或写入的缓冲区位置索引(类似于IO流中的记录指针)。当使用Buffer从Channel中读取数据时,position的值恰好等于已经读到了多少数据。
Buffer还支持一个可选的标记(mark),可以直接将position定位到该mark处。
- 0 <= mark <= position <= limit <= capacity
Buffer开始时position为0,limit为capacity,程序可通过put()方法向Buffer中放入一些数据,同时position相应向后移动一些位置。
当Buffer装入数据结束后,调用Buffer的flip()方法,该方法将limit设置为position所在位置,并将position设为0,这就使得Buffer的读写指针又移到了开始位置。也就是说,Buffer调用flip()方法之后,Buffer为输出数据做好准备,当Buffer输出数据结束后,Buffer调用clear()方法,该方法不是清空数据,它仅仅将position置0,将limit置为capacity,这样为再次向Buffer中装入数据做好准备。
其他方法参见API。
当使用put()和get()来访问Buffer中的数据时,分为相对和绝对两种:
- 相对(Relative):从Buffer的当前position处开始读取或写入数据,然后将position的位置按处理元素的个数增加
- 绝对(Absolute):直接根据索引向Buffer中读取或写入数据,使用绝对方式访问Buffer里的数据时,并不会影响position的值。
下面示范了Buffer的一些常规操作
//好像不需要用try catch块
//allocate()用于分配新的字符缓冲区
CharBuffer buff = CharBuffer.allocate(8);
//放入元素
buff.put('a');
buff.put('b');
buff.put('c');
//调用flip()方法
buff.flip();
//取出第一个元素
System.out.println(buff.get());
//调用clear()方法
buff.clear();
通过allocate()方法创建的Buffer对象是普通Buffer,ByteBuffer还提供了一个allocateDirect()方法来创建直接Buffer。直接Buffer的创建成本比普通Buffer创建成本高,但直接Buffer的读取效率更高,所以直接Buffer只适合用于长生存期的Buffer,而不适用于短生存期、一次用完就丢弃的Buffer,而且只有ByteBuffer才提供了allocateDirect()方法,如果希望使用其他类型,则应该将Buffer转换成其他类型的Buffer。
Channel类似于传统的流对象,但与传统的流对象有两个主要区别:
- Channel可以直接将指定文件的部分或全部直接映射成Buffer
- 程序不能直接访问Channel中的数据,无法读写。Channel只能与Buffer进行交互。
Java为Channel接口提供了许多实现类。Pipe.SinkChannel、Pipe.SourceChannel用于支持线程之间通信;ServerSocketChannel、SocketChannel用于支持TCP网络通信;DatagramChannel用于支持UDP网络通信。
所有的Channel都不应该通过构造器来直接创建,而是通过传统的节点InputStream、OutputStream的getChannel()方法来返回对应的Channel,不同的节点流获得的Channel不一样。
Channel中最常用的3个类方法:map()、read()和write()。map()用于将Channel对应的部分或全部数据映射成ByteBuffer。
下面程序示范了将FileChannel的全部数据映射成ByteBuffer的效果
File f = new File("FileChannelTest.java");
try{
//获取两个FileChannel用于输入和输出
FileChannel inChannel = new FileInputStream(f).getChannel();
FileChannel outChannel = new FileOutputStream("a.txt").getChannel();
//将FileChannel里的全部数据映射成ByteBuffer
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
//使用GBK的字符集来创建解码器
Charset charset = Charset.forName("GBK");
//直接将Buffer里的数据全部输出
outChannel.write(buffer);
//调用buffer的clear()方法,复原limit、position的位置
buffer.clear();
//创建解码器对象
CharsetDecoder decoder = charset.newDecoder();
//使用解码器将ByteBuffer转换成CharBuffer
CharBuffer charBuffer = decoder.decode(buffer);
//CharBuffer的toString方法可以获取对应的字符串
System.out.println(charBuffer);
//以下关闭的代码不知道是否全都得写
inChannel.close();
outChannel.close();
}
catch(IOException ex){
ex.printStackTrace();
}
虽然FileChannel能读也能写,但是FileInputStream获取的FileChannel只能读。FileOutputStream类似。RandomAccessFile返回的FileChannel是只读还是读写,取决于它打开文件的模式。
//把文件的内容复制一份追加到末尾
File f = new File("a.txt");
RandomAccessFile raf = new RandomAccessFile(f, "rw");
FileChannel randomChannel = raf.getChannel();
ByteBuffer buffer = randomChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
randomChannel.position(f.lenght());
randomChannel.write(buffer);
用传统IO流的过程来写Channel和Buffer:
FileInputStream fis = new FileInputStream("ReadFile.java");
FileChannel fcin = fis.getChannel();
ByteBuffer bbuff = ByteBuffer.allocate(64);
while(fcin.read(bbuff) != -1){
bbuff.flip();
Charset charset = Charset.forName("GBK");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer cbuff = decoder.decode(buff);
System.out.print(cbuff);
bbuff.clear();
}
编码(Encode):把明文的字符序列转换成计算机理解的二进制序列。
解码(Decode):与上相反。
Charset类提供了一个availableCharset()静态方法来获取当前JDK所支持的所有字符集。
常用的字符集别名:
- GBK:简体中文字符集
- BIG5:繁体中文字符集
- ISO-8859-1:ISO拉丁字母表No.1,也叫做ISO-LATIN-1
- UTF-8:8位UCS转换格式
- UTF-16BE:16位UCS转换格式,Big-endian(最高地址存放高位字节)
- UTF-16LE:16位UCS转换格式,Little-endian(最高地址存放低位字节)
- UTF-16:16位UCS转换格式,字节顺序由可选的字节顺序标记来标识
可以使用System类的getProperties()方法来访问本地系统的文件编码格式,文件格式的属性名为file.encoding。
程序可以调用Charset的forName()访求来创建对应的Charset对象。再通过该对象的newDecoder()、newEncoder()分别返回解码器CharsetDecoder和编码器CharsetEncoder。调用CharsetDecoder的decode()方法就可以将ByteBuffer(字节序列)转换成CharBuffer(字符序列),调用CharsetEncoder的encode()方法可以将CharBuffer或String(字符序列)转换成ByteBuffer
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
public class Test {
public static void main(String[] args) throws Exception {
Charset cn = Charset.forName("GBK");
CharsetEncoder cnEncoder = cn.newEncoder();
CharsetDecoder cnDecoder = cn.newDecoder();
CharBuffer cbuff = CharBuffer.allocate(8);
cbuff.put('孙');
cbuff.put('悟');
cbuff.put('空');
cbuff.flip();
ByteBuffer bbuff = cnEncoder.encode(cbuff);
for(int i = 0; i < bbuff.capacity(); i++){
System.out.println(bbuff.get(i) + " ");
}
System.out.println("\n" + cnDecoder.decode(bbuff));
}
}
Charset类也提供了decode()、encode()方法。如果仅需要进行简单的编码、解码操作,无须创建CharsetEncoder和CharsetDecoder对象,直接调用Charset的这两个方法即可。
String类中的getBytes()方法也是使用指定的字符集将字符串转换成字符序列。
以下代码向 baidu 发一次请求,并获取结果进行显示。例子演示到了 charset 的使用。
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.net.InetSocketAddress;
import java.io.IOException;
public class Test {
private Charset charset = Charset.forName("GBK");// 创建GBK字符集
private SocketChannel channel;
public void readHTMLContent() {
try {
InetSocketAddress socketAddress = new InetSocketAddress(
"www.baidu.com", 80);
//step1:打开连接
channel = SocketChannel.open(socketAddress);
//step2:发送请求,使用GBK编码
channel.write(charset.encode("GET " + "/ HTTP/1.1" + "\r\n\r\n"));
//step3:读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);// 创建1024字节的缓冲
while (channel.read(buffer) != -1) {
buffer.flip();// flip方法在读缓冲区字节操作之前调用。
System.out.println(charset.decode(buffer));
// 使用Charset.decode方法将字节转换为字符串
buffer.clear();// 清空缓冲
}
} catch (IOException e) {
System.err.println(e.toString());
} finally {
if (channel != null) {
try {
channel.close();
} catch (IOException e) {
}
}
}
}
public static void main(String[] args) {
new Test().readHTMLContent();
}
}
文件锁在操作系统中是很平常的事情,如果多个运行的程序需要并发修改同一个文件时,程序之间需要某种机制来进行通信,使用文件锁可以有效地阻止多个进程并发修改同一个文件,所以大部分操作系统都提供了文件锁的功能。
NIO中,Java提供了FileLock来支持文件锁定功能,在FileChannel中提供的lock()/tryLock()方法可以获得文件锁FileLock对象,从而锁定文件。lock()和tryLock()区别:lock()试图锁定某个文件时,如果无法得到文件锁,程序将一直阻塞;而tryLock()是尝试锁定文件,它将直接返回而不是阻塞,如果获得了文件锁,则该方法返回该文件锁,否则返回null。
lock()有个参数share为true时,表明该锁是一个共享锁,允许多个进程来读取该文件,但阻止其他进程获得对该文件的排他锁。当share为false时,表明该锁是一个排他锁,它将锁住对该文件的读写。程序可以通过调用FileLock的isShared来判断它获得的锁是否为共享锁。
直接使用无参数的lock()或tryLock()方法获取的文件锁是排他锁。
处理完文件用通过FileLock的release()方法释放文件锁。
FileChannel channel = new FileOutputStream("a.txt");
//非阻塞式对指定文件加锁
FileLock lock = channel.tryLock();
// 程序暂停10s,期间其他程序无法对a.txt文件进行修改
Thread.sleep(10000);
lock.release();
文件锁虽然可以用于控制并发访问,但对于高并发访问的情形,还是推荐使用数据库来保存程序信息,而不是使用文件。
还有如下几点注意:
- 在某些平台上,文件锁并不是强制性的,这意味着即使一个程序不能获得文件锁,它也可以对该文件进行读写。
- 在某些平台上,不能同步地锁定一个文件并把它映射到内存中
- 文件锁是Java虚拟机所持有的,如果两个Java程序使用同一个Java虚拟机运行,则它们不能对同一个文件进行加锁
- 某些平台上关闭FIleChannel时,会释放Java虚拟机在该文件上的所有锁,因此应该避免对一个被锁定的文件打开多个FileChannel
Java 7 对原有的NIO进行了重大改进,主要包括两个方面:
- 提供了全面的文件IO和文件系统访问支持。Java 7 新增的java.nio.file包及各个子包
- 基于异步Channel的IO。Java 7 在java.nio.channels包下增加了多个以Asynchronous开头的Channel接口和类。
Path接口代表一个平台无关的平台路径。Files包含了大量静态的工具方法来操作文件;Paths则包含了两个返回Path的静态工厂方法。
// 以当前路径来创建Path对象
Path path = Paths.get(".");
System.out.println("path里包含的路径数量:" + path.getNameCount());
System.out.println("path的根路径:" + path.getRoot());
//获取path对应的绝对路径
Path absolutePath = path.toAbsolutePath();
System.out.println(absolutePath);
//获取绝对路径的根路径
System.out.println("absolutePath的路径:" + absolutePath.getRoot());
// 获取绝对路径所包含的路径数量
System.out.println("absolutePath里包含的路径数量" + absolutePath.getNameCount());
System.out.println(absolutePath.getName(3));
// 以多个String来构建Path对象
Path path2 = Paths.get("g:" , "publish", "codes");
System.out.println(path2);
Files是一个操作文件的工具类。
// 复制文件
Files.copy(Paths.get("FilesTest.java"), new FileOutputStream("a.txt"));
// 判断FilesTest.java文件是否为隐藏文件
System.out.println("FilesTest.java是否为隐藏文件:" + Files.isHidden(Paths.get("FilesTest.java")));
//一次性读取FilesTest.java的所有行
List<String> lines = Files.readAllLines(Paths.get("FilesTest.java"), Charset.forName("gbk"));
System.out.println(lines);
// 判断指定文件的大小
Files.size(Paths.get("FileTest.java"));
List<String> poem = new ArrayList<String>();
poem.add("string1");
poem.add("string2");
//直接将多个字符串内容写入指定文件中
Files.write(Paths.get("pome.txt"), poem, Charset.forName("gbk"));
FileStroe cStore = Files.getFileStore(Paths.get("C:"));
//判断C盘的总空间、可用空间
cStroe.getTotalSpace();
cStore.getUsableSpace();
以前的Java版本中只能用递归遍历指定目录下的所有文件和子目录。
Java 7 的Files类提供了walkFileTree()方法来遍历文件和子目录。详见1.7 API。 该方法需要FileVisitor参数,它代表一个文件访问器,walkFileTree()方法会自动遍历start路径下的所有文件和子目录,遍历文件和子目录都会“触发”FileVistor中相应的方法。
FileVisitor中的方法详见1.7API,实际编程时没必要为FileVistor的4个方法都提供实现,可以通过继承SimpleFileVisitor(FileVisitor的实现类)来实现自己的“文件访问器”。
Files.walkFileTree(Paths.get("g:", "publish"), new SimpleFileVisitor<Path>(){
//访问文件时触发该方法
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException{
System.out.println("正在访问" + file + "文件");
//找到了FileVisitor.java文件
if(file.endsWith("FileVisitor.java")){
return FileVisitResult.TERMINATE;
}
return FileVisitResult.CONTINUE;
}
//开始访问目录时触发该方法
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException{
return FileVisitResult.CONTINUE;
}
});
以前的Java版本中,如果程序需要监控文件的变化,可以考虑启动一条后台线程,每隔一段时间去“遍历”一次指定目录中的文件,如果发现此次遍历结果与上次遍历的结果不同,则认为文件发生了变化,此法繁琐且性能不好。
NIO.2的Path类提供了一个register()方法来监听文件系统的变化。
- register(WatchService watcher, WatchEvent.King...events):watcher监听该目录下的文件变化,events参数指定要监听哪些类型的事件
使用register()方法完成注册后,可以调用 WatchService的3个方法来获取被监听目录的文件变化事件。
- WatchKey poll():获取下一个WatchKey,若无则返回null
- WatchKey poll(long timeout, TimeUnit unit):尝试等待timeout时间去获取下一个WatchKey
- WatchKey take():获取下一个WatchKey,若无则一直等待。 用于持续监控
package King.exercise;
import java.nio.file.FileSystems;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
public class WatchServiceTest {
public static void main(String[] args) throws Exception {
//获取文件系统的WatchService对象
WatchService watchService = FileSystems.getDefault().newWatchService();
//为C:盘根路径注册监听
Paths.get("C:/").register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
while(true){
//获取下一个文件变化事件
WatchKey key = watchService.take(); //没有就等待
for(WatchEvent<?> event : key.pollEvents()){
System.out.println(event.context() + "文件发生了" + event.kind() + "事件");
}
//重设WatchKey
boolean valid = key.reset();
//如果重设失败,退出监听
if(!valid) break;
}
}
}
上面程序用了一个死循环重复获取C盘根路径下文件的变化。
早期Java的File类可以访问一些简单的文件属性,如果要获取更多则须利用运行所在的平台的特定代码来实现。
Java 7 的NIO.2在java.nio.file.attribute包下提供了大量工具类,主要分为如下两类:
- XxxAttributeView :代表某种文件属性的“视图”
- XxxAttributes:代表某种文件属性的“集合”,程序一般通过XxxAttributeView对象来获取XxxAttributes。
FileAttributeView是其他XxxAttributeView的父接口。
- AclFileAttributeView:为特定文件设置ACL(Access Control List)及文件所有者属性它的getAcl()方法返回List对象,代表该文件的权限集。setAcl(List)方法修改。
- BasicFileAttributeView:获取或修改文件的基本属性,包括文件的最后修改时间、最后访问时间、创建时间、大小、是否为目录 、是否为符号链接等。它的readAttributes()方法返回一个BasicFileAttributes对象 ,对文件夹基本属性的修改是通过它完成的。
- DosFileAttributeView:获取或修改文件DOS相关属性,比如是否只读、是否隐藏、是否为系统文件、是否是存档文件等。其方法与BasicFileAttributeView类似
- FileOwnerAttributeView:获取或修改文件所有者
- PosixFileAttributeView:获取或修改POSIX(Portable Operating System Interface of INIX)属性。仅在UNIX、Linux等系统上有用。
- UserDefinedFileAttributeView:它可以让开发者为文件设置一些自定义属性。
package King.exercise;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.DosFileAttributeView;
import java.nio.file.attribute.FileOwnerAttributeView;
import java.nio.file.attribute.UserDefinedFileAttributeView;
import java.nio.file.attribute.UserPrincipal;
import java.util.Date;
import java.util.List;
public class Test {
public static void main(String[] args) throws Exception {
//获取将要操作的文件
Path testPath = Paths.get("AttributeViewTest.java");
//获取访问基本属性的BasicFileAttributeView
BasicFileAttributeView basicView = Files.getFileAttributeView(testPath, BasicFileAttributeView.class);
//获取访问基本属性的BasicFileAttributes
BasicFileAttributes basicAttribs = basicView.readAttributes();
//访问文件的基本属性,仅摘最关键部分
//创建时间
Date d = new Date(basicAttribs.creationTime().toMillis());
//最后访问时间
d = new Date(basicAttribs.lastAccessTime().toMillis());
//最后修改时间
d = new Date(basicAttribs.lastModifiedTime().toMillis());
//文件大小
long n = basicAttribs.size();
//获取访问文件属主信息的FileOwnerAttributeView
FileOwnerAttributeView ownerView = Files.getFileAttributeView(testPath, FileOwnerAttributeView.class);
//获取该文件所属的用户
System.out.println(ownerView.getOwner());
//获取系统中guest对应的用户
UserPrincipal user = FileSystems.getDefault().getUserPrincipalLookupService().lookupPrincipalByName("guest");
//修改用户
ownerView.setOwner(user);
//获取访问自定义属性的FileOwnerAttributeView
UserDefinedFileAttributeView userView = Files.getFileAttributeView(testPath, UserDefinedFileAttributeView.class);
List<String> attrNames = userView.list();
//遍历所有的自定义属性
for(String name : attrNames){
ByteBuffer buf = ByteBuffer.allocate(userView.size(name));
userView.read(name, buf);
buf.flip();
String value = Charset.defaultCharset().decode(buf).toString();
System.out.println(name + ":" + value);
}
//添加一个自定义属性
userView.write("发行者", Charset.defaultCharset().encode("king"));
//获取访问DOS属性的DosFileAttributeView
DosFileAttributeView dosView = Files.getFileAttributeView(testPath, DosFileAttributeView.class);
//设置隐藏、只读
dosView.setHidden(true);
dosView.setReadOnly(true);
}
}