14.3.4 Java8新增的重复注解

Java8以前,同一个程序元素前最多只能使用一个相同类型的注解;如果需要在同一个元素前使用多个相同类型的注解,则必须使用注解“容器”。例如在Struts2开发中,有时需要在Action类上使用多个@Result注解。在Java8以前只能写成如下形式:

1
2
3
4
5
@Result({
@Result(name="success",location="failed.jsp"),
@Result(name="success", location="CC.jsp")
})
public Acton FooAction{...}

上面代码中使用了两个@Result注解,但由于传统Java语法不允许多次使用@Result修饰同一个类,因此程序必须使用@Results注解作为两个@Result的容器,@Results注解包含一个类型为Result[]的成员变量value,程序指定的多个@Result将作为@Resultsvalue属性(数组类型)的数组元素

Java8开始,上面语法可以得到简化:Java8允许使用多个相同类型的注解来修饰同一个类,因此上面代码可能(之所以说可能,是因为重复注解还需要对原来的注解进行改造)可以简化为如下形式:

1
2
3
@Result(name="success",location="failed.jsp"),
@Result(name="success", location="CC.jsp")
public Acton FooAction{...}

开发重复注解需要使用@Repeatable修饰,下面通过示例来示范如何开发重复注解。首先定义一个FKTag注解。

1
2
3
4
5
6
7
8
9
10
11
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(FkTags.class)
public @interface FkTag {
// 为锟斤拷注锟解定锟斤拷2锟斤拷锟斤拷员锟斤拷锟斤拷
String name() default "锟斤拷锟斤拷锟斤拷锟�";

int age();
}

上面定义了FKTag注解,该注解包含两个成员变量。但该注解默认不能作为重复注解使用,如果使用两个以上的该注解修饰同一个类,编译器会报错。
为了将该注解改造成重复注解,需要使用@Repeatable修饰该注解,使用@Repeatable时必须为value成员变量指定值,该成员变量的值应该是一个“容器”注解——该“容器”注解可包含多个@FkTag,因此还需要定义如下的“容器”注解

1
2
3
4
5
6
7
8
9
10
import java.lang.annotation.*;

// 指定该注解信息会保留到运行时
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FkTags
{
// 定义value成员变量,该成员变量可接受多个@FkTag注解
FkTag[] value();
}

上面代码中,定义了一个FkTag[]类型的value成员变量,这意味着@FKTags注解的value成员变量可接受多个@FkTag注解,因此@FkTags注解可作为@FkTag的容器
修饰@FkTags注解的的注解:

1
@Retention(RetentionPolicy.RUNTIME)

指定@FKTags注解信息也可保留到运行时,这是必需的,因为:@FKTag注解信息需要保留到运行时,如果@FKTags注解只能保留到源代码级别(RetentionPolicy.SOURCE)或类文件(RetentionPolicy.CLASS),将会导致@FKTags的保留期小于@FkTag的保留期,如果程序将多个@FkTag注解放入@FkTags中,若JVM丢弃了@FKTags注解,自然也就丢弃了@FKTags的信息——而我们希望@FKTags注解可以保留到运行时,这就矛盾了。

容器注解保留期要更长

“容器”注解的保留期必须比它所包含的注解的保留期更长,否则编译器会报错
接下来程序可在定义@FkTag注解时添加如下修饰代码:

1
@Repeatable(FkTags.class);

经过上面步骤,就成功地定义了一个重复注解:@FkTag。读者可能已经发现,实际上@FkTag依然有“容器”注解,因此依然可用传统代码来使用该注解:

1
2
3
4
@FkTags({
@FkTag(age=5),
@FkTag(name="疯狂Java",age=9)
})

又由于@FKTag是重复注解,因此可直接使用两个@FKTag注解,如下代码所示

1
2
@FkTag(age=5),
@FkTag(name="疯狂Java",age=9)

实际上,第二种用法只是一个简化写法,系统依然将两个@FkTag注解作为@FkTagsvalue成员变量的数组元素。

程序 容器注解的本质

如下程序演示了重复注解的本质。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@FkTag(age = 5)
@FkTag(name = "疯狂Java", age = 9)
// @FkTags({@FkTag(age=5),
// @FkTag(name="疯狂Java" , age=9)})
public class FkTagTest {
public static void main(String[] args) {
Class<FkTagTest> clazz = FkTagTest.class;
/*
* 使用Java 8新增的getDeclaredAnnotationsByType()方法获取 修饰FkTagTest类的多个@FkTag注解
*/
FkTag[] tags = clazz.getDeclaredAnnotationsByType(FkTag.class);
// 遍历修饰FkTagTest类的多个@FkTag注解
for (FkTag tag : tags) {
System.out.println(tag.name() + "-->" + tag.age());
}
/*
* 使用传统的getDeclaredAnnotation()方法获取 修饰FkTagTest类的@FkTags注解
*/
FkTags container = clazz.getDeclaredAnnotation(FkTags.class);
System.out.println(container);
}
}

上面程序中的

1
FkTag[] tags = clazz.getDeclaredAnnotationsByType(FkTag.class);

这行代码,获取修饰FkTagTest类的多个@FkTag注解,此行代码使用的是Java8新增的getDeclaredAnnotationsByType()方法,该方法的功能与传统的getDeclaredAnnotation(()方法相同,只不过getDeclaredAnnotationsByType()方法相当于功能增强版,它可以获取多个重复注解,而getDeclaredAnnotation()方法则只能获取一个(在Java8以前,不允许出现重复注解)
上面程序中的

1
FkTags container = clazz.getDeclaredAnnotation(FkTags.class);

这行代码尝试获取修饰FkTagTest类的@ FkTags注解,虽然上面源代码中并未显式使用@FKTags注解,但由于程序使用了两个@FkTag注解修饰该类,因此系统会自动将两个@FKTag注解作为@FKTagsvalue成员变量的数组元素处理。
因此,第二行粗体字代码将可以成功地获取到@FkTags注解。
编译、运行程序,可以看到如下输出:

1
2
3
4
PS G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\14\14.3> java FkTagTest
疯狂软件-->5
疯狂Java-->9
@FkTags(value=[@FkTag(name=疯狂软件, age=5), @FkTag(name=疯狂Java, age=9)])

重复注解只是一种简化写法

重复注解只是一种简化写法,这种简化写法是一种假象:多个重复注解其实会被作为“容器”注解的value成员变量的数组元素。例如上面的重复的@FkTag注解其实会被作为@ ,FkTags注解的value成员变量的数组元素处理,

14.3 自定义注解

前面已经介绍了如何使用java.lang包下的4个基本的注解,下面介绍如何自定义注解,并利用注解来完成一些实际的功能

14.3.1 定义注解

示例 定义注解

定义新的注解类型使用@interface关键字(在原有的interface关键字前增加@符号)定义一个新的注解类型与定义一个接口非常像,如下代码可定义一个简单的注解类型。

1
2
3
4
//定义一个简单的注解类型
public @interface Test{

}

定义了该注解之后,就可以在程序的任何地方使用该注解,使用注解的语法非常类似于publicfinal这样的修饰符,通常可用于修饰程序中的类、方法、变量、接口等定义。通常会把注解放在所有修饰符之前,而且由于使用注解时可能还需要为成员变量指定值,因而注解的长度可能较长,所以通常把注解另放一行,如下程序所示

示例 注解修饰类

1
2
3
4
5
//使用 @Test修饰类定义
@Test
public class MyClass{

}

示例 注解修饰方法

在默认情况下,注解可用于修饰任何程序元素,包括类、接口、方法等,如下程序使用@Test来修饰方法

1
2
3
4
5
6
7
8
public class MyClass
{
//使用 @Test注解修饰方法
@Test
public void info (){

}
}

带成员变量的注解

注解不仅可以是这种简单的注解,还可以带成员变量,成员变量在注解定义中以无形参的方法形式来声明,其方法名和返回值定义了该成员变量的名字和类型。如下代码可以定义一个有成员变量的注解。

1
2
3
4
5
6
public @interface MyTag{
//定义带两个成员变量的注解
//注解中的成员变量以方法的形式来定义
String name();
int age();
}

自定义注解继承Annotation接口

可能有读者会看出,上面定义注解的代码与定义接口的语法非常像,只是MyTag使用 @interface关键字来定义,而接口使用interface来定义。
使用@interface定义的注解的确非常像定义了一个注解接口,这个注解接口继承了java.lang.annotation.Annotation接口,这一点可以通过反射看到MyTag接口里包含了java.lang.annotation.Annotation接口里的方法。

给注解的成员变量赋值

一旦在注解里定义了成员变量之后,使用该注解时就应该为它的成员变量指定值,如下代码所示。

1
2
3
4
5
6
7
public class Test{
//使用带成员变量的注解时,需要为成员变量赋值
@MyTag(name="xx", age=6);
public void info(){

}
}

为注解的成员变量指定默认值

也可以在定义注解的成员变量时为其指定初始值(默认值),指定成员变量的初始值可使用default关键字。如下代码定义了@MyTag注解,该注解里包含了两个成员变量:nameage,这两个成员变量使用default指定了初始值。

1
2
3
4
5
6
7
public @interface MyTag
{
//定义了两个成员变量的注解
//使用 default为两个成员变量指定初始值
string name() default "HelloWorld";
int age() default 32;
}

如果为注解的成员变量指定了默认值,使用该注解时则可以不为这些成员变量指定值,而是直接使用默认值。

1
2
3
4
5
6
7
8
9
public class Test
{
//使用带成员变量的注解
//因为它的成员变量有默认值,所以可以不为它的成员变量指定值
@MyTag
public void info(){

}
}

当然也可以在使用MyTag注解时为成员变量指定值,如果为MyTag的成员变量指定了值,则默认值不会起作用。

根据有无成员变量为注解分类

根据注解是否可以包含成员变量,可以把注解分为如下两类。

  1. 标记注解:没有定义成员变量的注解类型被称为标记。这种注解仅利用自身的存在与否来提供信息,如前面介绍的@Override@Test等注解。
  2. 元数据注解:包含成员变量的注解,因为它们可以接受更多的元数据,所以也被称为元数据注解,

14.2 JDK的元注解

JDK除在java.lang下提供了5个基本的注解之外,还在java.lang.annotation包下提供了6个Meta注解(元注解,其中有5个元注解都用于修饰其他的注解定义。其中@Repeatable专门用于定义Java8新增的重复注解,本章后面会重点介绍相关内容。此处先介绍常用的4个元注解。

java.lang.annotation包中用于修饰其他注解定义的注解

  • @Retention
  • @Target
  • @Documented
  • @Inherited

14.2.1 使用@Retention

@Retention只能用于修饰注解定义,用于指定被修饰的注解可以保留多长时间,@Retention包含一个RetentionPolicy类型的value成员变量,所以使用@Retention时必须为该value成员变量指定值。

value成员变量的值只能是如下三个

属性 描述
RetentionPolicy.CLASS 编译器将把注解记录在class文件中。当运行Java程序时,JVM不可获取注解信息。这是默认值。
RetentionPolicy.RUNTIME 编译器将把注解记录在class文件中。当运行Java程序时,JVM也可获取注解信息,程序可以通过反射获取该注解信息。
RetentionPolicy.SOURCE 注解只保留在源代码中,编译器直接丢弃这种注解。

如果需要通过反射获取注解信息,就需要使用value属性值为RetentionPolicy.RUNTIME@Retention。使用@Retention元注解可采用如下代码为value指定值

1
2
3
定义下面的 @Testable注解保留到运行时
@Retention(value= RetentionPolicy.RUNTIME)
public @interface Testable{}

也可采用如下代码来为value指定值。

1
2
3
//定义下面的 @Testable注解将被编译器直接丢弃
@Retention(RetentionPolicy.SOURCE)
public @interface Testables{}

当注解的成员变量名为value时可以省略value

上面代码中使用@Retention元注解时,并未通过value=RetentionPolicy.SOURCE的方式来为该成员变量指定值,这是因为当注解的成员变量名为value时,程序中可以直接在注解后的括号里指定该成员变量的值,无须使用name=Value的形式

如果使用注解时只需要为value成员变量指定值,则使用该注解时可以直接在该注解后的括号里指定value成员变量的值,无须使用“value=变量值”的形式

14.2.2 使用@Target

Target也只能修饰注解定义,它用于指定被修饰的注解能用于修饰哪些程序单元@Target元注解也包含一个名为value的成员变量,该成员变量的值只能是如下几个。

属性 描述
ElementType.ANNOTATION_TYPE 指定该策略的注解只能修饰注解。
ElementType.CONSTRUCTOR 指定该策略的注解只能修饰构造器
ElementType.FIELD 指定该策略的注解只能修饰成员变量。
ElementType.LOCAL_VARIABLE 指定该策略的注解只能修饰局部变量
ElementType.METHOD 指定该策略的注解只能修饰方法定义
ElementType.PACKAGE 指定该策略的注解只能修饰包定义。
ElementType.PARAMETER 指定该策略的注解可以修饰参数。
ElementType.TYPE 指定该策略的注解可以修饰类、接口(包括注解类型)或枚举定义。

与使用@Retention类似的是,使用@Target也可以直接在括号里指定value值,而无须使用name=Value的形式。

代码 指定一个注解只能修饰成员变量

如下代码指定@ActionListenerFor注解只能修饰成员变量。

1
2
@Target(ElementType.FIELD)
public @interface ActionListenerFor{}

代码 指定一个注解只能修饰方法

如下代码片段指定@Testable注解只能修饰方法。

1
2
@Target(ElementType.METHOD);
public @interface Testable{}

14.2.3 使用@Documented

@Documented用于指定被@Documented注解修饰的注解类将被javadoc工具提取成文档,如果定义注解类时使用了@Documented修饰,则所有使用该注解修饰的程序元素的API文档中将会包含该注解说明。

程序

下面代码定义了一个Testable注解,程序使用 @Documented来修饰tEstable注解定义,所以该注解将被javadoc工具所提取。

1
2
3
4
5
6
7
8
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
// 定义Testable注解将被javadoc工具提取
@Documented
public @interface Testable {
}

上面代码中的粗体字代码指定了javadoc工具生成的API文档将提取@Testable的使用信息下面代码定义了一个MyTest类,该类中的inf()方法使用了@Testable修饰。

1
2
3
4
5
6
7
public class MyTest {
// 使用@Test修饰info方法
@Testable
public void info() {
System.out.println("info方法...");
}
}

使用javadoc工具为Testable.javaMyTest.java文件生成API文档

1
javadoc Testable.java MyTest.java -d api

效果如图14.1所示。

如果把上面Testable.java程序中的注解@Documented删除或注释掉,再次使用javadoc工具生成的API文档如图14.2所示

对比图14.1和142所示两份API文档中灰色区域覆盖的info()方法说明,图1411中的info()方法说明里包含了@Testable的信息,这就是使用@DocumentedAnnotation的作用

14.2.4 使用@Inherited

@Inherited元注解指定被它修饰的注解将具有继承性:如果某个类使用了@Xxx注解(定义该注解时使用了@Inherited修饰)修饰,则其子类将自动被@Xxx修饰。

程序

下面使用 @Inherited元注解修饰@Inheritable定义,则该注解将具有继承性。

1
2
3
4
5
6
7
import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Inheritable {
}

上面程序表明@Inheritable具有继承性,如果某个类使用了@Inheritable修饰,则该类的子类将自动使用 @Inheritable修饰。

下面程序中定义了一个Base基类,该基类使用了 @Inheritable修饰,则Base类的子类将会默认使用 @Inheritable修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用@Inheritable修饰的Base类
@Inheritable
class Base {
}

// TestInheritable类只是继承了Base类,
// 并未直接使用@Inheritable Annotiation修饰
public class InheritableTest extends Base {
public static void main(String[] args) {
// 打印TestInheritable类是否具有@Inheritable修饰
System.out.println(InheritableTest.class.isAnnotationPresent(Inheritable.class));
}
}

上面程序中的Base类使用了@Inheritable修饰,而该注解具有继承性,所以其子类也将自动使用@Inheritable修饰。运行上面程序,会看到输出:true
如果将InheritableTest.java程序中的粗体字代码注释掉或者删除,将会导致@Inheritable不具有继承性。运行上面程序,将看到输出:false

14.1 基本注解

注解必须使用工具来处理,工具负责提取注解里包含的元数据,工具还会根据这些元数据增加额外的功能。在系统学习新的注解语法之前,先看一下Java提供的5个基本注解的用法:使用注解时要在其前面增加@符号,并把该注解当成一个修饰符使用,用于修饰它支持的程序元素。

5个基本注解

5个基本的注解如下:

  • @Override
  • @Deprecated
  • @SuppressWarnings
  • @SafeVarargs
  • @FunctionalInterface

上面5个基本注解中的 @SafeVarargsJava7新增的、@FunctionalInterfaceJava8新增的。

这5个基本定义在java.lang包中

这5个基本的注解都定义在java.lang包下,读者可以通过查阅它们的API文档来了解关于它们的更多细节。

14.1.1 限定重写父类方法:@Override

@Override就是用来指定方法覆载的,它可以强制一个子类必须覆盖父类的方法。

程序

如下程序中使用@Override指定子类Appleinfo()方法必须重写父类方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Fruit {
public void info() {
System.out.println("水果的info方法...");
}
}

class Apple extends Fruit {
// 使用@Override指定下面方法必须重写父类方法
@Override
// public void inf0() {
public void info() {
System.out.println("苹果重写水果的info方法...");
}
}

@Override可避免方法重写时 写错方法签名

编译上面程序,可能丝毫看不出程序中的@Override有何作用,因为@Override的作用是告诉编译器检査这个方法,保证父类要包含一个被该方法重写的方法,否则就会编译出错。@Override主要是帮助程序员避免一些低级错误,例如把上面Apple类中的info()方法不小心写成了inf0,这样的“低级错误”可能会成为后期排错时的巨大障碍
如果把Apple类中的info()方法误写成inf0(),编译程序时将出现如下错误提示

1
2
3
4
5
G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\14\14.1>javac -encoding utf-8 Fruit.java
Fruit.java:9: 错误: 方法不会覆盖或实现超类型的方法
@Override
^
1 个错误

@Override只能修饰方法

@Override只能修饰方法,不能修饰其他程序元素。

14.1.2 Java9增强的@Deprecated

@Deprecated用于表示某个程序元素(类、方法等)已过时,当其他程序使用已过时的类、方法时,编译器将会给出警告。

Java9增加的属性

Java9@Deprecated注解增加了如下两个属性:

属性 描述
boolean forRemoval 指定该API在将来是否会被删除。
String since 指定该API从哪个版本被标记为过时。

程序

如下程序指定Apple类中的info方法已过时,其他程序中使用Apple类的info法时编译器将会给出警告

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Apple {
// 定义info方法已过时
// since属性指定从哪个版本开始,forRemoval指定该API将来会被删除
@Deprecated(since = "9", forRemoval = true)
public void info() {
System.out.println("Apple的info方法");
}
}

public class DeprecatedTest {
public static void main(String[] args) {
// 下面使用info方法时将会被编译器警告
new Apple().info();
}
}

上面程序中使用了Appleinfo()方法,而Apple类中定义info()方法时使用了@Deprecated修饰,表明该方法已过时,所以将会引起编译器警告。

@Deprecated@deprecated文档标记的区别

@Deprecated的作用与文档注释中的@deprecated标记的作用基本相同,但它们的用法不同:
@DeprecatedJDK5才支持的注解,无须放在文档注释语法(/**......*/部分)中,而是直接用于修饰程序中的程序单元,如方法、类、接口等

14.1.3 抑制编译器警告:@SuppressWarnings

@SuppressWarnings指示被该注解修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告@SuppressWarnings会一直作用于该程序元素的所有子元素,例如,使用@SuppressWarnings修饰某个类取消显示某个编译器警告,同时又修饰该类里的某个方法取消显示另一个编译器警告,那么该方法将会同时取消显示这两个编译器警告。

程序

1
2
3
4
5
6
7
8
import java.util.*;

@SuppressWarnings(value = "unchecked")
public class SuppressWarningsTest {
public static void main(String[] args) {
List<String> myList = new ArrayList(); // ①
}
}

程序中的粗体字代码使用@SuppressWarnings来关闭SuppressWarningsTest类里的所有编译器警告,编译上面程序时将不会看到任何编译器警告。如果删除程序中的@SuppressWarnings(value = "unchecked")注解,将会在程序的①处看到编译器警告:

1
2
3
G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\14\14.1>javac -encoding utf-8 SuppressWarningsTest.java  
注: SuppressWarningsTest.java使用了未经检查或不安全的操作。
注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。

正如从程序中粗体字代码所看到的,当使用@SuppressWarnings注解来关闭编译器警告时,一定要在括号里使用name=Value的形式为该注解的成员变量设置值。关于如何为注解添加成员变量请看下节介绍。

14.1.4 “堆污染”警告与Java9增强的@SafeVarargs

前面介绍泛型擦除时,介绍了如下代码可能导致运行时异常。

堆污染

Java把引发这种错误的原因称为“堆污染”(HeapPollution),当把一个不带泛型的对象赋给一个带泛型的变量时,往往就会发生这种“堆污染”,如上①号粗体字代码所示。
对于形参个数可变的方法,该形参的类型又是泛型,这将更容易导致“堆污染”

程序

例如如下工具类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.*;

public class ErrorUtils {
// @SafeVarargs
public static void faultyMethod(List<String>... listStrArray) {
// Java语言不允许创建泛型数组,因此listArray只能被当成List[]处理
// 此时相当于把List<String>赋给了List,已经发生了“擦除”
List[] listArray = listStrArray;
List<Integer> myList = new ArrayList<Integer>();
myList.add(new Random().nextInt(100));
// 把listArray的第一个元素赋为myList
listArray[0] = myList;
String s = listStrArray[0].get(0);
}
}

上面程序中的代码:

1
List[] listArray = listStrArray;

已经发生了“堆污染”。由于该方法有个形参是List<String>类型,个数可变的形参相当于数组,但Java又不支持泛型数组,因此程序只能把List<String>当成List[]处理,这里就发生了“堆污染”。
Java6以及更早的版本中,Java编译器认为faultyMethod()方法完全没有问题,既不会提示错误也没有提示警告。
等到使用该方法时,例如如下程序。

1
2
3
4
5
6
7
import java.util.*;

public class ErrorUtilsTest {
public static void main(String[] args) {
ErrorUtils.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));//①
}
}

编译该程序将会在①号代码处引发一个unchecked警告。这ˆunchecked警告岀现得比较“突兀”定义faultyMethod()方法时没有任何警告,调用该方法时却引发了一个“警告”
上面程序故意利用了“堆污染”,因此程序运行时也会在①号代码处引发ClassCastException异常。
Java7开始,Java编译器将会进行更严格的检查,Java编译器在编译ErrorUtils时就会发出一个如下所示的警告

1
2
3
G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\14\14.1>javac -encoding utf-8 ErrorUtils.java
注: ErrorUtils.java使用了未经检查或不安全的操作。
注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。

由此可见,Java7会在定义该方法时就发出“堆污染”警告,这样保证开发者“更早”地注意到程序中可能存在的“漏洞”

抑制堆污染警告的三种方法

但在有些时候,开发者不希望看到这个警告,则可以使用如下三种方式来“抑制”这个警告。

  • 使用@SafeVarargs修饰引发该警告的方法或构造器。Java9增强了该注解,允许使用该注解修饰私有实例方法。
  • 使用@SuppressWarnings("unchecked")修饰。
  • 编译时使用-Xlint:unchecked选项。

很明显,第三种方式一般比较少用,通常可以选择第一种或第二种方式,尤其是使用@SafeVarargs修饰引发该警告的方法或构造器,它是Java7专门为抑制“堆污染”警告提供的。
如果程序使用@SafeVarargs修饰ErrorUtils类中的faultyMethod()方法,则编译上面两个程序时都不会发出任何警告

14.1.5 Java8的函数式接口与@FunctionalInterface

前面已经提到,Java8规定:只有一个抽象方法的接口就是函数式接口(注意:函数式接口可以包含多个默认方法或多个static方法)
@FunctionalInterface就是用来指定某个接口必须是函数式接口
函数式接口就是为Java8Lambda表达式准备的,Java8允许使用Lambda表达式创建函数式接口的实例,因此Java8专门增加了@ .Functionalnterface

程序

例如,如下程序使用@FunctionalInterface修饰了函数式接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@FunctionalInterface
public interface FunInterface {
static void foo() {
System.out.println("foo类方法");
}

default void bar() {
System.out.println("bar默认方法");
}

void test(); // 只定义一个抽象方法
// 再定义抽象方法
// void abc();
}

编译上面程序,可能丝毫看不出程序中的@FunctionalInterface有何作用,因为@FunctionalInterface只是告诉编译器检査这个接口,保证该接口只能包含一个抽象方法,否则就会编译出错。@FunctionalInterface主要是帮助程序员避免一些低级错误,例如,在上面的FunInterface接口中再增加一个抽象方法abc(),编译程序时将出现如下错误提示:

@FunctionalInterface只能修饰接口,不能修饰其他程序元素。

1
2
3
4
5
6
7
G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\14\14.1>javac -encoding utf-8 FunInterface.java
FunInterface.java:1: 错误: 意外的 @FunctionalInterface 注释
@FunctionalInterface
^
FunInterface 不是函数接口
在 接口 FunInterface 中找到多个非覆盖抽象方法
1 个错误

14.0 本章概述

本章要点

  • 注解的概念和作用
  • @Override注解的功能和用法
  • @Deprecated注解的功能和用法
  • @SuppressWarnings注解的功能和用法
  • @Retention注解的功能和用法
  • @Target注解的功能和用法
  • @Documented注解的功能和用法
  • @Inherited注解的功能和用法
  • 自定义注解
  • 提取注解信息
  • 重复注解
  • 类型注解
  • 使用APT工具

元数据MetaData 注解

JDK5开始,Java增加了对元数据(MetaData)的支持,也就是Annotation(即注解,也被翻译为注释),这种注解与第3章所介绍的注释有一定的区别。本章所介绍的注解,其实是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。
通过使用注解,程序开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署。
注解提供了一种为程序元素设置元数据的方法,从某些方面来看,注解就像修饰符一样,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明,这些信息被存储在注解的“name=value”对中。

如何获取注解里的元数据

注解是一个接口,程序可以通过反射来获取指定程序元素的java.lang.annotation.Annotation对象,然后通过java.lang.annotation.Annotation对象来取得注解里的元数据。

注解不影响代码的执行

注解能被用来为程序元素(类、方法、成员变量等)设置元数据。值得指岀的是,注解不影响程序代码的执行,无论増加、删除注解,代码都始终如一地执行。

APT

如果希望让程序中的注解在运行时起一定的作用,只有通过某种配套的工具对注解中的信息进行访问和处理,访问和处理注解的工具统称APT(Annotation Processing Tool)

18.7 本章小结

本章详细介绍了Java反射的相关知识。本章内容对于普通的Java学习者而言,确实显得有点深入,并且会感觉不太实用。但随着知识的慢慢积累,当读者希望开发出更多基础的、适应性更广的、灵活性更强的代码时,就会想到使用反射知识了。
本章从类的加载、初始化开始,深入介绍了Java类加载器的原理和机制。本章重点在于介绍Class,MethodFieldConstructorTypeParameterizedType等类和接口的用法,包括动态创建Java实例和动态调用Java对象的方法。
本章介绍的两个对象工厂实际上就是Spring框架的核心,希望读者用心揣摩。

本章也介绍了利用PoxyInvocationHandler来创建JDK动态代理,并详细介绍了JDK动态代理和AOP之间的关系,这也是Java灵活性的重要方面,对于提高系统解耦也十分重要,希望读者能用心掌握。

本章练习

  1. 开发一个工具类,该工具类提供一个eval方法,实现JavaScripteval(函数的功能——可以动态运行一行或多行程序代码。例如eval("Systen.out.printIn(\"aa\")"),将输出aa
  2. 开发一个对象工厂池,这个对象工厂池不仅可以管理对象的String类型成员变量的值,还可以管理容器中对象的其他类型成员变量的值,甚至可以将对象的成员变量设置成引用到容器中其他对象(这就是Spring所提出的控制反转,即loC)。

12.13 本章小结

本章与前一章内容的结合性非常强,本章主要介绍了以AWT为基础的Swing编程知识。本章简要介绍了Swing基本组件如对话框、按钮的用法,还详细介绍了Swing所提供的特殊容器。除此之外,本章重点介绍了Swing提供的特殊控件:JLitJComboBoxSPinnerJSliderJTableJre等,介绍JTableJTre时深入介绍了SwingMVC实现机制,并通过提供自定义的Render来改变页面JTableJTree的外观效果。

本章练习

  1. 设计俄罗斯方块游戏
  2. 设计仿ACDSee的看图程序。
  3. 结合JTreeJListJSplitPaneJDesktopPaneJInternalframeJTextPane等组件,开发仿Editplus的文字编辑程序界面,可以暂时不提供文字保存、文字打开等功能。

使用EditPlusEclipse等工具时会发现,当在这些工具中输入代码时,如果输入的单词是程序关键字、类名等,则这些关键字将会自动变色。使用JTextPane组件,就可以开发出这种带有语法高亮的编辑器
JTextPane使用StyledDocument作为它的model对象,而StyleDocument允许对文档的不同段落分别设置不同的颜色、字体属性

Document使用Element来表示文档中的组成部分,Element可以表示章(chapter)、段落(paragraph)等.
在普通文档中,Element也可以表示一行。

AttributeSet接口

为了设置StyledDocument中文字的字体、颜色,Swing提供了AttributeSet接口来表示文档字体、颜色等属性

SwingStyledDocument提供了DefaultstyledDocument实现类,该实现类就是JTextPanemodel实现类;为AttributeSet接口提供了MutableAttributeSet子接口,并为该接口提供了SimpleAttributeSet实现类,程序通过这些接口和实现类就可以很好地控制JTextPane中文字的字体和颜色。

StyledDocument

StyledDocument提供了如下一个方法来设置文档中局部文字的字体、颜色

方法 描述
void setParagraphAttributes(int offset, int length, AttributeSet s, boolean replace) 设置文档中从offset开始,长度为length处的文字使用s属性(控制字体、颜色等),最后一个参数控制新属性是替换原有属性,还是将新属性累加到原有属性上。

StyleConstants工具类

AttributeSet的常用实现类是MutableAttributeSet,为了给MutableAttributeSet对象设置字体、颜色等属性,Swing提供了StyleConstants工具类,该工具类里大致包含了如下常用的静态方法来设置MutableAttributeSet里的字体、颜色等。

方法 描述
static void setAlignment(MutableAttributeSet a, int align) 设置文本对齐方式。
static void setBackground(MutableAttributeSet a, Color fg) 设置背景色
static void setBold(MutableAttributeSet a, boolean b) 设置是否使用粗体字
static void setFirstLineIndent(MutableAttributeSet a, float i) 设置首行缩进的大小。
static void setFontFamily(MutableAttributeSet a, String fam) 设置字体。
static void setFontSize(MutableAttributeSet a, int s) 设置字体大小。
static void setForeground(MutableAttributeSet a, Color fg) 设置字体前景色。
static void setItalic(MutableAttributeSet a, boolean b) 设置是否采用斜体字。
static void setLeftIndent(MutableAttributeSet a, float i) 设置左边缩进大小。
static void setLineSpacing(MutableAttributeSet a, float i) 设置行间距。
static void setRightIndent(MutableAttributeSet a, float i) 设置右边缩进大小。
static void setStrikeThrough(MutableAttributeSet a, boolean b) 设置是否为文字添加删除线。
static void setSubscript(MutableAttributeSet a, boolean b) 设置将指定文字设置成下标。
static void setSuperscript(MutableAttributeSet a, boolean b) 设置将指定文字设置成上标。
static void setUnderline(MutableAttributeSet a, boolean b) 设置是否为文字添加下画线。

上面这些方法用于控制文档中文字的外观样式,如果读者对这些外观样式不是太熟悉,则可以参考Word里设置“字体”属性的设置效果。
图12.60显示了Document及其相关实现类,以及相关辅助类的类关系图。

程序 文本编辑器设置 字体 颜色

下面程序简单地定义了三个SimpleAttributeSet对象,并为这三个对象设置了对应的文字、颜色、字体等属性,并使用三个SimpleAttributeSet对象设置文档中三段文字的外观。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.awt.*;
import javax.swing.*;
import javax.swing.text.*;

public class JTextPaneTest {
JFrame mainWin = new JFrame("测试JTextPane");
JTextPane txt = new JTextPane();
StyledDocument doc = txt.getStyledDocument();
// 定义三个SimpleAttributeSet对象
SimpleAttributeSet android = new SimpleAttributeSet();
SimpleAttributeSet java = new SimpleAttributeSet();
SimpleAttributeSet javaee = new SimpleAttributeSet();

public void init() {
// 为android属性集设置颜色、字体大小、字体和下划线
StyleConstants.setForeground(android, Color.RED);
StyleConstants.setFontSize(android, 24);
StyleConstants.setFontFamily(android, "Dialog");
StyleConstants.setUnderline(android, true);
// 为java属性集设置颜色、字体大小、字体和粗体字
StyleConstants.setForeground(java, Color.BLUE);
StyleConstants.setFontSize(java, 30);
StyleConstants.setFontFamily(java, "Arial Black");
StyleConstants.setBold(java, true);
// 为javaee属性集设置颜色、字体大小、斜体字
StyleConstants.setForeground(javaee, Color.GREEN);
StyleConstants.setFontSize(javaee, 32);
StyleConstants.setItalic(javaee, true);
// 设置不允许编辑
txt.setEditable(false);
txt.setText("疯狂Android讲义\n" + "疯狂Java讲义\n" + "轻量级Java EE企业应用实战\n");
// 分别为文档中三段文字设置不同的外观样式
doc.setCharacterAttributes(0, 12, android, true);
doc.setCharacterAttributes(12, 12, java, true);
doc.setCharacterAttributes(24, 30, javaee, true);
mainWin.add(new JScrollPane(txt), BorderLayout.CENTER);
// 获取屏幕尺寸
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
int inset = 100;
// 设置主窗口的大小
mainWin.setBounds(inset, inset, screenSize.width - inset * 2, screenSize.height - inset * 2);
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.setVisible(true);
}

public static void main(String[] args) {
new JTextPaneTest().init();
}
}

上面程序其实很简单,程序中的第一段粗体字代码为三个SimpleAttributeSet对象设置了字体、字体大小、颜色等外观样式,第二段粗体字代码使用前面的三个SimpleAttributeSet对象来控制文档中三段文字的外观样式。运行上面程序,将看到如图12.61所示的界面。

图12.61

从图12.61中可以看出,窗口中文字具有丰富的外观,而且还可以选中这些文字,表明它们依然是文字,而不是直接绘制上去的图形。
如果希望开发出类似于EditplusEclipse等的代码编辑窗口,程序可以扩展JTextPane的子类,为该对象添加按键监听器和文档监听器。当文档内容被修改时,或者用户在该文档内进行击键动作时,程序负责分析该文档的内容,对特殊关键字设置字体颜色。

为了保证具有较好的性能,程序并不总是分析文档中的所有内容,而是只分析文档中被改变的部分,这个要求看似简单,只为文档添加文档监听器即可:当文档内容改变时分析被改变部分,并设置其中关键字的颜色。但问题是:Documentlistener监听器里的三个方法不能改变文档本身,所以程序还是必须通过监听按键事件来启动语法分析,DocumentListener监听器中仅仅记录文档改变部分的位置和长度。

除此之外,程序还提供了一个SyntaxFormatter类根据语法文件来设置文档中的文字颜色。

程序 带语法高亮的文本编辑器 单词着色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
import java.util.*;
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;

public class MyTextPane extends JTextPane {
private static final long serialVersionUID = 7177281290506716762L;
protected StyledDocument doc;
protected SyntaxFormatter formatter = new SyntaxFormatter("my.stx");
// 定义该文档的普通文本的外观属性
private SimpleAttributeSet normalAttr = formatter.getNormalAttributeSet();
private SimpleAttributeSet quotAttr = new SimpleAttributeSet();
// 保存文档改变的开始位置
private int docChangeStart = 0;
// 保存文档改变的长度
private int docChangeLength = 0;

public MyTextPane() {
StyleConstants.setForeground(quotAttr, new Color(255, 0, 255));
StyleConstants.setFontSize(quotAttr, 16);
this.doc = super.getStyledDocument();
// 设置该文档的页边距
this.setMargin(new Insets(3, 40, 0, 0));
// 添加按键监听器,当按键松开时进行语法分析
this.addKeyListener(new KeyAdapter() {
public void keyReleased(KeyEvent ke) {
syntaxParse();
}
});
// 添加文档监听器
doc.addDocumentListener(new DocumentListener() {
// 当Document的属性或属性集发生了改变时触发该方法
public void changedUpdate(DocumentEvent e) {
}

// 当向Document中插入文本时触发该方法
public void insertUpdate(DocumentEvent e) {
docChangeStart = e.getOffset();
docChangeLength = e.getLength();
}

// 当从Document中删除文本时触发该方法
public void removeUpdate(DocumentEvent e) {
}
});
}

public void syntaxParse() {
try {
// 获取文档的根元素,即文档内的全部内容
Element root = doc.getDefaultRootElement();
// 获取文档中光标插入符的位置
int cursorPos = this.getCaretPosition();
int line = root.getElementIndex(cursorPos);
// 获取光标所在位置的行
Element para = root.getElement(line);
// 定义光标所在行的行头在文档中位置
int start = para.getStartOffset();
// 让start等于start与docChangeStart中较小值。
start = start > docChangeStart ? docChangeStart : start;
// 定义被修改部分的长度
int length = para.getEndOffset() - start;
length = length < docChangeLength ? docChangeLength + 1 : length;
// 取出所有可能被修改的字符串
String s = doc.getText(start, length);
// 以空格、点号等作为分隔符
String[] tokens = s.split("\\s+|\\.|\\(|\\)|\\{|\\}|\\[|\\]");
// 定义当前分析单词的在s字符串中的开始位置
int curStart = 0;
// 定义单词是否处于引号以内
boolean isQuot = false;
for (String token : tokens) {
// 找出当前分析单词在s字符串中的位置
int tokenPos = s.indexOf(token, curStart);
if (isQuot && (token.endsWith("\"") || token.endsWith("\'"))) {
doc.setCharacterAttributes(start + tokenPos, token.length(), quotAttr, false);
isQuot = false;
} else if (isQuot && !(token.endsWith("\"") || token.endsWith("\'"))) {
doc.setCharacterAttributes(start + tokenPos, token.length(), quotAttr, false);
} else if ((token.startsWith("\"") || token.startsWith("\'"))
&& (token.endsWith("\"") || token.endsWith("\'"))) {
doc.setCharacterAttributes(start + tokenPos, token.length(), quotAttr, false);
} else if ((token.startsWith("\"") || token.startsWith("\'"))
&& !(token.endsWith("\"") || token.endsWith("\'"))) {
doc.setCharacterAttributes(start + tokenPos, token.length(), quotAttr, false);
isQuot = true;
} else {
// 使用格式器对当前单词设置颜色
formatter.setHighLight(doc, token, start + tokenPos, token.length());
}
// 开始分析下一个单词
curStart = tokenPos + token.length();
}
} catch (Exception ex) {
ex.printStackTrace();
}
}

// 重画该组件,设置行号
public void paint(Graphics g) {
super.paint(g);
Element root = doc.getDefaultRootElement();
// 获得行号
int line = root.getElementIndex(doc.getLength());
// 设置颜色
g.setColor(new Color(230, 230, 230));
// 绘制显示行数的矩形框
g.fillRect(0, 0, this.getMargin().left - 10, getSize().height);
// 设置行号的颜色
g.setColor(new Color(40, 40, 40));
// 每行绘制一个行号
for (int count = 0, j = 1; count <= line; count++, j++) {
g.drawString(String.valueOf(j), 3, (int) ((count + 1) * 1.535 * StyleConstants.getFontSize(normalAttr)));
}
}

public static void main(String[] args) {
JFrame frame = new JFrame("文本编辑器");
// 使用MyTextPane
frame.getContentPane().add(new JScrollPane(new MyTextPane()));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
final int inset = 50;
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
frame.setBounds(inset, inset, screenSize.width - inset * 2, screenSize.height - inset * 2);
frame.setVisible(true);
}
}

// 定义语法格式器
class SyntaxFormatter {
// 以一个Map保存关键字和颜色的对应关系
private Map<SimpleAttributeSet, ArrayList<String>> attMap = new HashMap<>();
// 定义文档的正常文本的外观属性
SimpleAttributeSet normalAttr = new SimpleAttributeSet();

public SyntaxFormatter(String syntaxFile) {
// 设置正常文本的颜色、大小
StyleConstants.setForeground(normalAttr, Color.BLACK);
StyleConstants.setFontSize(normalAttr, 16);
// 创建一个Scanner对象,负责根据语法文件加载颜色信息
Scanner scaner = null;
try {
scaner = new Scanner(new File(syntaxFile));
} catch (FileNotFoundException e) {
throw new RuntimeException("丢失语法文件:" + e.getMessage());
}
int color = -1;
ArrayList<String> keywords = new ArrayList<>();
// 不断读取语法文件的内容行
while (scaner.hasNextLine()) {
String line = scaner.nextLine();
// 如果当前行以#开头
if (line.startsWith("#")) {
if (keywords.size() > 0 && color > -1) {
// 取出当前行的颜色值,并封装成SimpleAttributeSet对象
SimpleAttributeSet att = new SimpleAttributeSet();
StyleConstants.setForeground(att, new Color(color));
StyleConstants.setFontSize(att, 16);
// 将当前颜色和关键字List对应起来
attMap.put(att, keywords);
}
// 重新创建新的关键字List,为下一个语法格式准备
keywords = new ArrayList<>();
color = Integer.parseInt(line.substring(1), 16);
} else {
// 对于普通行,每行内容添加到关键字List里
if (line.trim().length() > 0) {
keywords.add(line.trim());
}
}
}
// 把所有关键字和颜色对应起来
if (keywords.size() > 0 && color > -1) {
SimpleAttributeSet att = new SimpleAttributeSet();
StyleConstants.setForeground(att, new Color(color));
StyleConstants.setFontSize(att, 16);
attMap.put(att, keywords);
}
}

// 返回该格式器里正常文本的外观属性
public SimpleAttributeSet getNormalAttributeSet() {
return normalAttr;
}

// 设置语法高亮
public void setHighLight(StyledDocument doc, String token, int start, int length) {
// 保存需要对当前单词对应的外观属性
SimpleAttributeSet currentAttributeSet = null;
outer: for (SimpleAttributeSet att : attMap.keySet()) {
// 取出当前颜色对应的所有关键字
ArrayList<String> keywords = attMap.get(att);
// 遍历所有关键字
for (String keyword : keywords) {
// 如果该关键字与当前单词相同
if (keyword.equals(token)) {
// 跳出循环,并设置当前单词对应的外观属性
currentAttributeSet = att;
break outer;
}
}
}
// 如果当前单词对应的外观属性不为空
if (currentAttributeSet != null) {
// 设置当前单词的颜色
doc.setCharacterAttributes(start, length, currentAttributeSet, false);
}
// 否则使用普通外观来设置该单词
else {
doc.setCharacterAttributes(start, length, normalAttr, false);
}
}
}

配置文件

展开/折叠
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#FF0000
System
Class
String
Integer
Object


#0000FF
abstract
assert
boolean
break
byte
case
catch
char
class
const
continue
default
do
double
else
enum
extends
false
final
finally
float
for
goto
if
import
implements
int
interface
instanceof
long
native
new
null
package
private
protected
public
return
short
static
strictfp
super
switch
synchronized
this
throw
throws
transient
try
true
void
volatile
while

上面程序中的粗体字代码负责分析当前单词与哪种颜色关键字匹配,并为这段文字设置字体颜色。其实这段程序为文档中的单词设置颜色并不难,难点在于找出每个单词与哪种关键字匹配,并要标识出该单词在文档中的位置,然后才可以为该单词设置颜色。
运行上面程序,会看到如图12.62所示的带语法高亮的文本编辑器

图12.62

上面程序已经完成了对不同类型的单词进行着色,所以会看到如图12.62示的运行界面。如果进行改进,则可以为上面的编辑器增加括号配对、代码折叠等功能,这些都可以通过JTextPane组件来完成对于此文本编辑器,只要传入不同的语法文件,程序就可以为不同的源代码显示语法高亮

12.12.4 使用

Swing提供了一个JEditorPane类,该类可以编辑各种文本内容,包括有格式的文本。在默认情况下,JEditorPane支持如下三种文本内容。

JEditorPane支持的文本内容 描述
text/plain 纯文本,当JEditorPane无法识别给定内容的类型时,使用这种文本格式。在这种模式下,文本框的内容是带换行符的无格式文本。
text/html HTML文本格式。该文本组件仅支持HTML3.2格式,因此对互联网上复杂的网页支持非常有限。
text/rtf RTF(富文本格式)文本格式。实际上,它对RTF的支持非常有限。

JEditorPane用途有限

通过上面介绍不难看出,其实JEditorPane类的用途非常有限,使用JEditorPane作为纯文本的编辑器,还不如使用JTextArea;如果使用JEditorPane来支持RTF文本格式,但它对这种文本格式的支持又相当有限;JEditorPane唯一可能的用途就是显示自己的HTML文档,前提是这份HTML文档比较简单,只包含HTML3.2或更早的元素

JEditorPane组件支持三种方法来加载文本内容。

  • 使用getText()方法直接设置JEditorPane的文本内容。
  • 使用read()方法从输入流中读取JEditorPane的文本内容。
  • 使用setPage()方法来设置JEditorPane从哪个URL处读取文本内容。在这种情况下,将根据该URL来确定内容类型。

在默认状态下,使用JEditorPane装载的文本内容是可编辑的,即使装载互联网上的网页也是如此,可以使用JEditorPanesetEditable(false)方法阻止用户编辑该JEditorPane里的内容。
当使用JEditorPane打开HTML页面时,该页面的超链接是活动的,用户可以单击超链接。如果程序想监听用户单击超链接的事件,则必须使用addHyperlinkListener()方法为JEditorPane添加一个HyperlinkListener监听器。

程序

从目前的功能来看,JEditorPane确实没有太大的实用价值,所以本书不打算给出此类的用法示例有兴趣的读者可以参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.awt.*;
import javax.swing.*;
import javax.swing.text.*;

public class JTextPaneTest {
JFrame mainWin = new JFrame("测试JTextPane");
JTextPane txt = new JTextPane();
StyledDocument doc = txt.getStyledDocument();
// 定义三个SimpleAttributeSet对象
SimpleAttributeSet android = new SimpleAttributeSet();
SimpleAttributeSet java = new SimpleAttributeSet();
SimpleAttributeSet javaee = new SimpleAttributeSet();

public void init() {
// 为android属性集设置颜色、字体大小、字体和下划线
StyleConstants.setForeground(android, Color.RED);
StyleConstants.setFontSize(android, 24);
StyleConstants.setFontFamily(android, "Dialog");
StyleConstants.setUnderline(android, true);
// 为java属性集设置颜色、字体大小、字体和粗体字
StyleConstants.setForeground(java, Color.BLUE);
StyleConstants.setFontSize(java, 30);
StyleConstants.setFontFamily(java, "Arial Black");
StyleConstants.setBold(java, true);
// 为javaee属性集设置颜色、字体大小、斜体字
StyleConstants.setForeground(javaee, Color.GREEN);
StyleConstants.setFontSize(javaee, 32);
StyleConstants.setItalic(javaee, true);
// 设置不允许编辑
txt.setEditable(false);
txt.setText("疯狂Android讲义\n" + "疯狂Java讲义\n" + "轻量级Java EE企业应用实战\n");
// 分别为文档中三段文字设置不同的外观样式
doc.setCharacterAttributes(0, 12, android, true);
doc.setCharacterAttributes(12, 12, java, true);
doc.setCharacterAttributes(24, 30, javaee, true);
mainWin.add(new JScrollPane(txt), BorderLayout.CENTER);
// 获取屏幕尺寸
Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
int inset = 100;
// 设置主窗口的大小
mainWin.setBounds(inset, inset, screenSize.width - inset * 2, screenSize.height - inset * 2);
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.setVisible(true);
}

public static void main(String[] args) {
new JTextPaneTest().init();
}
}

来学习该类的用法。相比之下,该类的子类JTextPane则功能丰富多了,下面详细介绍JTextPane类的用法。

合理输入问题

在有些情况下,程序不希望用户在输入框内随意地输入,例如,程序需要用户输入一个有效的时间,或者需要用户输入一个有效的物品价格,如果用户输入不合理,程序应该阻止用户输入

使用事件监听实现

对于这种需求,通常的做法是为该文本框添加失去焦点的监听器,再添加回车按键的监听器,当该文本框失去焦点时,或者该用户在该文本框内按回车键时,就检测用户输入是否合法。这种做法基本可以解决该问题,但编程比较烦琐!

使用JFormattedTextField

Swing提供的JFormattedTextField可以更优雅地解决该问题。

JFormattedTextField和普通文本行的区别

使用JFormattedTextField与使用普通文本行有一个区别:

  • 它需要指定一个文本格式,只有当用户的输入满足该格式时,JFormattedTextField才会接收用户输入

JFormattedTextField格式分类

JFormattedTextField可以使用如下两种类型的格式。

  • JFormattedTextField.AbstractFormatter:该内部类有一个子类DefaultFormatter,而DefaultFormatter又有一个非常实用的MaskFormatter子类,允许程序以掩码的形式指定文本格式
  • Format:主要由DateFormatNumberFormat两个格式器组成,这两个格式器可以指定JFormattedTextField所能接收的格式字符串。

JFormattedTextField方法

创建JFormattedTextField对象时可以传入上面任意一个格式器:

构造器 描述
JFormattedTextField(Format format) Creates a JFormattedTextField.

成功地创建了JFormattedTextField对象之后,JFormattedTextField对象的用法和普通TextField的用法基本相似,一样可以调用setColumns()来设置该文本框的宽度,调用setFont()来设置该文本框内的字体等。

除此之外,JFormattedTextField还包含如下三个特殊方法

JFormattedTextField类的方法 描述
Object getValue() 获取该格式化文本框里的值
void setValue(Object value) 设置该格式化文本框的初始值
void setFocusLostBehavior(int behavior) 设置该格式化文本框失去焦点时的行为。

getValue方法

上面三个方法中获取格式化文本框内容的getValue方法返回Object类型,而不是返回String类型;

setValue方法

与之对应的是,设置格式化文本框初始值的setValue方法需要传入Object类型参数,而不是String类型参数。
这都是因为格式化文本框会将文本框内容转换成指定格式对应的对象,而不再是普通字符串。

setFocusLostBehavior方法

setFocusLostBehavior方法可以接收如下4个值:

setFocusLostBehavior方法参数值 描述
JFormattedTextField.COMMIT 如果用户输入的内容满足格式器的要求,则该格式化文本框显示的文本变成用户输入的内容,调用getValue()方法返回的是该文本框内显示的内容;如果用户输入的内容不满足格式器的要求,则该格式化文本框显示的依然是用户输入的内容,但调用getValue()方法返回的不是该文本框内显示的内容,而是上一个满足要求的值
JFormattedTextField.COMMIT_OR_REVERT 这是默认值。如果用户输入的内容满足格式器的要求,则该格式化文本框显示的文本、getValue()方法返回的都是用户输入的内容;如果用户输入的内容不满足格式器的要求,则该格式化文本框显示的文本、getValue()方法返回的都是上一个满足要求的值。
JFormattedTextField.REVERT 不管用户输入的内容是否满足格式器的要求,该格式化文本框都显示用户输入的内容,getValue()方法返回的都是上一个满足要求的值。
JFormattedTextField.PERSIST 不管用户输入的内容是否满足格式器的要求,该格式化文本框显示的内容、getValue()方法返回的都是上一个满足要求的值。在这种情况下,不管用户输入什么内容对该文本框都没有任何影响。

DefaultFormatter

DefaultFormatter是一个功能非常强大的格式器,它可以格式化任何类的实例,只要该类包含带一个字符串参数的构造器,并提供对应的toString()方法(该方法的返回值就是传入给构造器字符串参数的值)即可

例如,URL类包含一个URL(String spec)构造器,且URL对象的toString()方法恰好返回刚刚传入的spec参数,因此可以使用DefaultFormatter来格式化URL对象。当格式化文本框失去焦点时,该格式器就会调用带一个字符串参数的构造器来创建新的对象,如果构造器抛出了异常,即表明用户输入无效。

DefaultFormatter格式器默认采用改写方式来处理用户输入,即当用户在格式化文本框内输入时,每输入一个字符就会替换文本框内原来的一个字符。如釆想关闭这种改写方式,采用插入方式,则可通过调用它的setOverwriteMode(false)方法来实现。

MaskFormatter

MaskFormatter格式器的功能有点类似于正则表达式,它要求用户在格式化文本框内输入的内容必须匹配一定的掩码格式。例如,若要匹配广州地区的电话号码,则可采用020-########的格式,这个掩码字符串和正则表达式有一定的区别,因为该掩码字符串只支持如下通配符。

  • #:代表任何有效数字
  • ':转义字符,用于转义具有特殊格式的字符。例如,若想匹配#,则应该写成'#
  • U:任何字符,将所有小写字母映射为大写。
  • L:任何字符,将所有大写字母映射为小写。
  • A:任何字符或数字。
  • ?:任何字符。
  • *:可以匹配任何内容。
  • H:任何十六进制字符(0~9a~fA~F)。

值得指出的是,格式化文本框内的字符串总是和掩码具有相同的格式,连长度也完全相同。如果用户删除了格式化文本框内的字符,这些被删除的字符将由占位符替代。默认使用空格作为占位符,当然也可以调用MaskFormattersetPlaceholderCharacter()方法来设置该格式器的占位符。例如如下代码:

1
formatter.setPlaceholdercharacter(' ');

程序

下面程序示范了关于JFormattedTextField的简单用法。

1

上面程序添加了6个格式化文本框,其中两个是基于NumberFormat生成的整数格式器、货币格式器,两个是基于DateFormat生成的日期格式器,一个是使用DefaultFormatter创建的URL格式器,最后一个是使用MaskFormatter创建的掩码格式器,程序中的粗体字代码是创建这些格式器的关键代码。

除此之外,程序还添加了4个单选按钮,用于控制这些格式化文本框失去焦点后的行为。运行上面程序,并选中“COMMIT”行为,将看到如图12.59所示的界面

从图12.59中可以看出,虽然用户向格式化文本框内输入的内容与该文本框所要求的格式不符,但该文本框依然显示了用户输入的内容,只是后面显示该文本框的getValue()方法返回值时看到的依然是100,即上一个符合格式的值。

大部分时候,使用基于Format的格式器,DefaultFormatterMaskFormatter已经能满足绝大部分要求;但对于一些特殊的要求,则可以采用扩展DefaultFormatter的方式来定义自己的格式器。定义自己的格式器通常需要重写DefaultFormatter的如下两个方法

方法 描述
Object stringToValue(String string) 根据格式化文本框内的字符串来创建符合指定格式的对象。
String valueToString(Object value) 将符合格式的对象转换成文本框中显示的字符串

例如,若需要创建一个只能接收IP地址的格式化文本框,则可以创建一个自定义的格式化文本框,因为IP地址是由4个0~255之间的整数表示的,所以程序采用长度为4的byte数组来保存IP地址,程序可以采用如下方法将用户输入的字符串转换成byte数组。

如何保证用户输入的有效性

除此之外,Swing还提供了如下两种机制来保证用户输入的有效性

  • 输入过滤:输入过滤机制允许程序拦截用户的插入、替换、删除等操作,并改变用户所做的修改。
  • 输入校验:输入验证机制允许用户离开输入组件时,验证机制自动触发——如果用户输入不符合要求,校验器强制用户重新输入。

输入过滤器

输入过滤器需要继承DocumentFilter类,程序可以重写该类的如下三个方法来拦截用户的插入、删除和替换等操作。

方法 描述
void insertString(DocumentFilter.FilterBypass fb, int offset, String string, AttributeSet attr) 该方法会拦截用户向文档中插入字符串的操作。
void remove(DocumentFilter.FilterBypass fb, int offset, int length) 该方法会拦截用户从文档中删除字符串的操作。
void replace(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attrs) 该方法会拦截用户替换文档中字符串的操作。

输入校验器

为了创建自己的输入校验器,可以通过扩展InputVerifier类来实现。实际上,InputVerifier输入校验器可以绑定到任何输入组件,InputVerifier类里包含了一个verify(JComponent component)方法,当用户在该输入组件内输入完成,且该组件失去焦点时,该方法被调用,如果该方法返回false,即表明用户输入无效,该输入组件将自动得到焦点。也就是说,如果某个输入组件绑定了InputVerifier,则用户必须为该组件输入有效内容,否则用户无法离开该组件

有一种情况例外,如果输入焦点离开了带InputVerifier输入校验器的组件后,立即单击某个按钮,则该按钮的事件监听器将会在焦点重新回到原组件之前被触发。

程序 为格式化文本框 添加输入过滤器 输入校验器

下面程序示范了如何为格式化文本框添加输入过滤器、输入校验器,程序还自定义了一个IP地址格式器,该IP地址格式器扩展了DefaultFormatter格式器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import java.util.*;
import java.text.*;
import java.net.*;
import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.text.*;

public class JFormattedTextFieldTest
{
private JFrame mainWin = new JFrame("测试格式化文本框");
private JButton okButton = new JButton("确定");
// 定义用于添加格式化文本框的容器
private JPanel mainPanel = new JPanel();
JFormattedTextField[] fields = new JFormattedTextField[6];
String[] behaviorLabels = new String[]
{
"COMMIT",
"COMMIT_OR_REVERT",
"PERSIST",
"REVERT"
};
int[] behaviors = new int[]
{
JFormattedTextField.COMMIT,
JFormattedTextField.COMMIT_OR_REVERT,
JFormattedTextField.PERSIST,
JFormattedTextField.REVERT
};
ButtonGroup bg = new ButtonGroup();
public void init()
{
// 添加按钮
JPanel buttonPanel = new JPanel();
buttonPanel.add(okButton);
mainPanel.setLayout(new GridLayout(0, 3));
mainWin.add(mainPanel, BorderLayout.CENTER);
// 使用NumberFormat的integerInstance创建一个JFormattedTextField
fields[0] = new JFormattedTextField(NumberFormat
.getIntegerInstance());
// 设置初始值
fields[0].setValue(100);
addRow("整数格式文本框 :", fields[0]);
// 使用NumberFormat的currencyInstance创建一个JFormattedTextField
fields[1] = new JFormattedTextField(NumberFormat
.getCurrencyInstance());
fields[1].setValue(100.0);
addRow("货币格式文本框:", fields[1]);
// 使用默认的日期格式创建一个JFormattedTextField对象
fields[2] = new JFormattedTextField(DateFormat.getDateInstance());
fields[2].setValue(new Date());
addRow("默认的日期格式器:", fields[2]);
// 使用SHORT类型的日期格式创建一个JFormattedTextField对象,
// 且要求采用严格日期格式
DateFormat format = DateFormat.getDateInstance(DateFormat.SHORT);
// 要求采用严格的日期格式语法
format.setLenient(false);
fields[3] = new JFormattedTextField(format);
fields[3].setValue(new Date());
addRow("SHORT类型的日期格式器(语法严格):", fields[3]);
try
{
// 创建默认的DefaultFormatter对象
DefaultFormatter formatter = new DefaultFormatter();
// 关闭overwrite状态
formatter.setOverwriteMode(false);
fields[4] = new JFormattedTextField(formatter);
// 使用DefaultFormatter来格式化URL
fields[4].setValue(new URL("http://www.crazyit.org"));
addRow("URL:", fields[4]);
}
catch (MalformedURLException e)
{
e.printStackTrace();
}
try
{
MaskFormatter formatter = new MaskFormatter("020-########");
// 设置占位符
formatter.setPlaceholderCharacter('□');
fields[5] = new JFormattedTextField(formatter);
// 设置初始值
fields[5].setValue("020-28309378");
addRow("电话号码:", fields[5]);
}
catch (ParseException ex)
{
ex.printStackTrace();
}

JPanel focusLostPanel = new JPanel();
// 采用循环方式加入失去焦点行为的单选按钮
for (int i = 0; i < behaviorLabels.length ; i++ )
{
final int index = i;
final JRadioButton radio = new JRadioButton(behaviorLabels[i]);
// 默认选中第二个单选按钮
if (i == 1)
{
radio.setSelected(true);
}
focusLostPanel.add(radio);
bg.add(radio);
// 为所有单选按钮添加事件监听器
radio.addActionListener(e -> {
// 如果当前该单选按钮处于选中状态,
if (radio.isSelected())
{
// 设置所有的格式化文本框的失去焦点的行为
for (int j = 0 ; j < fields.length ; j++)
{
fields[j].setFocusLostBehavior(behaviors[index]);
}
}
});
}
focusLostPanel.setBorder(new TitledBorder(new EtchedBorder(),
"请选择焦点失去后的行为"));
JPanel p = new JPanel();
p.setLayout(new BorderLayout());
p.add(focusLostPanel , BorderLayout.NORTH);
p.add(buttonPanel , BorderLayout.SOUTH);

mainWin.add(p , BorderLayout.SOUTH);
mainWin.pack();
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.setVisible(true);
}
// 定义添加一行格式化文本框的方法
private void addRow(String labelText, final JFormattedTextField field)
{
mainPanel.add(new JLabel(labelText));
mainPanel.add(field);
final JLabel valueLabel = new JLabel();
mainPanel.add(valueLabel);
// 为"确定"按钮添加事件监听器
// 当用户单击“确定”按钮时,文本框后
okButton.addActionListener(event -> {
Object value = field.getValue();
// 输出格式化文本框的值
valueLabel.setText(value.toString());
});
}
public static void main(String[] args)
{
new JFormattedTextFieldTest().init();
}
}

运行上面程序,会看到窗口中出现三个格式化文本框,其中第一个格式化文本框只能输入数字,其他字符无法输入到该文本框内;第二个格式化文本框有输入校验器,只有当用户输入的内容符合该文本框的要求时,用户才可以离开该文本框;第三个格式化文本框的格式器是自定义的格式器,它要求用户输入的内容是一个合法的IP地址。