16.1 线程概述

几乎所有的操作系统都支持同时运行多个任务,一个任务通常就是一个程序,每个运行中的程序就是一个进程
当一个程序运行时,内部可能包含了多个顺序执行流,每个顺序执行流就是一个线程。

16.1.1 线程和进程

什么是进程

几乎所有的操作系统都支持进程的概念,所有运行中的任务通常对应一个进程(Process)。
当一个程序进入内存运行时,即变成一个进程进程是处于运行过程中的程序,并且具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位

进程的三个特征

一般而言,进程包含如下三个特征。

  • 独立性:进程是系统中独立存在的实体,它可以拥有自己独立的资源,每一个进程都拥有自己私有的地址空间。在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间。
  • 动态性:进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合。在进程中加入了时间的概念。进程具有自己的生命周期和各种不同的状态,这些概念在程序中都是不具备的。
  • 并发性:多个进程可以在单个处理器上并发执行,多个进程之间不会互相影响。

并发性和并行性的区别

并发性(concurrency)和并行性(parallel)是两个概念,

  • 并行指在同一时刻,有多条指令在多个处理器上同时执行;
  • 并发指在同一时刻,只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。

进程快速轮回执行

对于一个CPU而言,它在某个时间点只能执行一个程序,也就是说,只能运行一个进程,CPU不断地在这些进程之间轮换执行。不过因为CPU的执行速度相对人的感觉来说实在是太快了,所以虽然CPU在多个进程之间轮换执行,但用户感觉到好像有多个进程在同时执行。不过如果启动的程序足够多,用户依然可以感觉到程序的运行速度下降。

多进程并发策略

现代的操作系统都支持多进程的并发,但在具体的实现细节上可能因为硬件和操作系统的不同而采用不同的策略。比较常用的方式有:

  • 共用式的多任务操作策略,例如Windows3.1Mac OS9;
  • 抢占式多任务操作策略,这种策略效率更高,目前操作系统大多采用这种策略,例如Windows NTWindows 2000以及UNIX/Linux等操作系统。

多线程

多线程则扩展了多进程的概念,使得同一个进程可以同时并发处理多个任务。

线程是进程的组成部分

线程(Thread)也被称作轻量级进程(Lightweight Process),线程是进程的执行单元。就像进程在操作系统中的地位一样,线程在程序中是独立的、并发的执行流
当进程被初始化后,主线程就被创建了。对于绝大多数的应用程序来说,通常仅要求有一个主线程,但也可以在该进程内创建多条顺序执行流,这些顺序执行流就是线程,每个线程也是互相独立的
线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程

线程拥有的资源

线程可以拥有自己的堆栈、自己的程序计数器和自己的局部变量

线程共享父线程的系统资源

线程不拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。因为多个线程共享父进程里的全部资源,因此编程更加方便;但必须更加小心,因为需要确保线程不会妨碍同一进程里的其他线程。
线程可以完成一定的任务,可以与其他线程共享父进程中的共享变量及部分环境,相互之间协同来完成进程所要完成的任务。

线程独立运行

线程是独立运行的,它并不知道进程中是否还有其他线程存在。线程的执行是抢占式的,也就是说,当前运行的线程在任何时候都可能被挂起,以便另外一个线程可以运行。
一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。
从逻辑角度来看,多线程存在于一个应用程序中,让一个应用程序中可以有多个执行部分同时执行,但操作系统无须将多个线程看作多个独立的应用,对多线程实现调度和管理以及资源分配。线程的调度和管理由进程本身负责完成

一个程序至少有一个进程 一个进程至少有一个线程

简而言之,一个程序运行后至少有一个进程,一个进程里可以包含多个线程,但至少要包含一个线程

归纳起来可以这样说:操作系统可以同时执行多个任务,每个任务就是进程;进程可以同时执行多个任务,每个任务就是线程。

16.1.2 多线程的优势

线程在程序中是独立的、并发的执行流,与分隔的进程相比,进程中线程之间的隔离程度要小。它们共享内存、文件句柄和其他每个进程应有的状态。
因为线程的划分尺度小于进程,使得多线程程序的并发性高。进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。

线程共享的环境

线程比进程具有更高的性能,这是由于同一个进程中的线程都有共性:一多个线程共享同一个进程虚拟空间。线程共享的环境包括:进程代码段进程的公有数据。利用这些共享的数据,线程很容易实现相互之间的通信

当操作系统创建一个进程时,必须为该进程分配独立的内存空间,并分配大量的相关资源;但创建一个线程则简单得多,因此使用多线程来实现并发比使用多进程实现并发的性能要高得多。

多线程编程优点

总结起来,使用多线程编程具有如下几个优点。

  • 进程之间不能共享内存,但线程之间共享内存非常容易
  • 系统创建进程时需要为该进程重新分配系统资源,但创建线程则代价小得多,因此使用多线程来实现多任务并发比多进程的效率高
  • Java语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java的多线程编程。

线程常见用途

在实际应用中,多线程是非常有用的:

  • 一个浏览器必须能同时下载多个图片;
  • 一个Web服务器必须能同时响应多个用户请求;
  • Java虚拟机本身就在后台提供了一个超级线程来进行垃圾回收;
  • 图形用户界面(GUI)应用也需要启动单独的线程从主机环境收集用户界面事件…

总之,多线程在实际编程中的应用是非常广泛的。

第16章 多线程

本章要点

  • 线程的基础知识
  • 理解线程和进程的区别与联系
  • 三种创建线程的方式
  • 线程的run()方法和start方法的区别与联系
  • 线程的生命周期
  • 线程死亡的几种情况
  • 控制线程的常用方法
  • 线程同步的概念和必要性
  • 使用synchronized控制线程同步
  • 使用Lock对象控制线程同步
  • 使用Object提供的方法实现线程通信
  • 使用条件变量实现线程通信
  • 使用阻塞队列实现线程通信
  • 线程组的功能和用法
  • 线程池的功能和用法
  • Java8增强的ForkJoinPool
  • ThreadLocal类的功能和用法
  • 使用线程安全的集合类
  • Java9新增的发布-订阅框架

前面大部分程序,都只是在做单线程的编程,前面所有程序(除第11章、第12章的程序之外,它们有内建的多线程支持)都只有一条顺序执行流——程序从main()方法开始执行,依次向下执行每行代码,如果程序执行某行代码时遇到了阻塞,则程序将会停滞在该处。如果使用IDE工具的单步调试功能,就可以非常清楚地看出这一点

但实际的情况是,单线程的程序往往功能非常有限,例如开发一个简单的服务器程序,这个服务器程序需要向不同的客户端提供服务时,不同的客户端之间应该互不干扰,否则会让客户端感觉非常沮丧。

多线程听上去是非常专业的概念,其实非常简单:

  • 单线程的程序(前面介绍的绝大部分程序)只有一个顺序执行流,
  • 多线程的程序则可以包括多个顺序执行流,多个顺序流之间互不干扰。

可以这样理解:

  • 单线程的程序如同只雇佣一个服务员的餐厅,他必须做完一件事情后才可以做下一件事情;
  • 多线程的程序则如同雇佣多个服务员的餐厅,他们可以同时做多件事情

Java语言提供了非常优秀的多线程支持,程序可以通过非常简单的方式来启动多线程。
本章将会详细介绍Java多线程编程的相关方面,包括创建、启动线程、控制线程,以及多线程的同步操作,并会介绍如何利用Java内建支持的线程池来提高多线程性能。

18.6 反射和泛型

JDK5以后,JavaClass类增加了泛型功能,从而允许使用泛型来限制Class类,例如, String.class的类型实际上是Class<String>。如果Class对应的类暂时未知,则使用Classs<?>通过在反射中使用泛型,可以避免使用反射生成的对象需要强制类型转换

18.6.1 泛型和Class类

使用Class<T>泛型可以避免强制类型转换。

程序 简单对象工厂

例如,下面提供一个简单的对象工厂,该对象工厂可以根据指定类来提供该类的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CrazyitObjectFactory {
public static Object getInstance(String clsName) {
try {
// 创建指定类对应的Class对象
Class cls = Class.forName(clsName);
// 返回使用该Class对象所创建的实例
return cls.newInstance();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

问题 需要强制类型转换

上面程序中try块中的代码代码根据指定的字符串类型创建了一个新对象,但这个对象的类型是Object,因此当需要使用CrazyitObjectFactorygetInstance()方法来创建对象时,将会看到如下代码

1
2
//获取实例后需要强制类型转换
Date d =(Date) CrazyitObjectFactory.getInstance("java.util.Date");

甚至出现如下代码:

1
JFrame f=(JFrame )CrazyitObjectFactory.getInstance("java.util.Date");

上面代码在编译时不会有任何问题,但运行时将抛出ClassCastException异常,因为程序试图将个Date对象转换成JFrame对象。
如果将上面的CrazyitobjectFactory工厂类改写成使用泛型后的Class,就可以避免这种情况。

程序 使用泛型后的对象工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

import java.util.*;
import javax.swing.*;

public class CrazyitObjectFactory2 {
public static <T> T getInstance(Class<T> cls) {
try {
return cls.newInstance();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

public static void main(String[] args) {
// 获取实例后无须类型转换
Date d = CrazyitObjectFactory2.getInstance(Date.class);
JFrame f = CrazyitObjectFactory2.getInstance(JFrame.class);
}
}

在上面程序的getInstance方法中传入一个Class<T>参数,这是一个泛型化的Class对象,调用该Class对象的newInstance()方法将返回一个T对象。接下来当使用CrazyitObjectFactory2工厂类的getInstance()方法来产生对象时,无须使用强制类型转换,系统会执行更严格的检查,不会出现ClassCastException运行时异常.

Array类的创建数组方法(newInstance方法) 存在强制类型转换问题

前面介绍使用Array类来创建数组时,曾经看到如下代码:

1
2
//使用 Array的 newInstance方法来创建一个数组
Object arr = Array.newInstance(String.class, 10);

对于上面的代码其实使用并不是非常方便,因为newInstance()方法返回的确实是一个String数组,而不是简单的Object对象。如果需要将arr对象当成String数组使用,则必须使用强制类型转换,这是不安全的操作。
奇怪的是,ArraynewInstance()方法签名为如下形式:

1
static Object newInstance(Class<?> componentType, int length)

在这个方法签名中使用了Class<?>泛型,但并没有真正利用这个泛型;如果将该方法签名改为如下形式:

1
public static <T> T[] newInstance(Class<T> componentType, int length);

这样就可以在调用该方法后无须强制类型转换了

程序 包装Array类的newInstance方法解决强制类型转换问题

为了示范泛型的优势,可以对ArraynewInstance()方法进行包装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.lang.reflect.*;

public class CrazyitArray {
// 对Array的newInstance方法进行包装
@SuppressWarnings("unchecked")
public static <T> T[] newInstance(Class<T> componentType, int length) {
return (T[]) Array.newInstance(componentType, length); // ①
}

public static void main(String[] args) {
// 使用CrazyitArray的newInstance()创建一维数组
String[] arr = CrazyitArray.newInstance(String.class, 10);
// 使用CrazyitArray的newInstance()创建二维数组
// 在这种情况下,只要设置数组元素的类型是int[]即可。
int[][] intArr = CrazyitArray.newInstance(int[].class, 5);
arr[5] = "HelloWorld";
// intArr是二维数组,初始化该数组的第二个数组元素
// 二维数组的元素必须是一维数组
intArr[1] = new int[] { 23, 12 };
System.out.println(arr[5]);
System.out.println(intArr[1][1]);
}
}

上面程序中粗体字代码定义的newInstance()方法对Array类提供的newInstance()方法进行了包装,将方法签名改成了
public static <T> T[] newInstance(Class<T> componentType, int length),
这就保证程序通过该newInstance()方法创建数组时的返回值就是数组对象,而不是Object对象,从而避免了强制类型转换。
程序在①行代码处将会有一个unchecked编译警告,所以程序使用了@SuppressWarnings来抑制这个警告信息。

18.6.2 使用反射来获取泛型信息

通过指定类对应的Class对象,可以获得该类里包含的所有成员变量,不管该成员变量是使用private修饰,还是使用public修饰。获得了成员变量对应的Field对象后,就可以很容易地获得该成员变量的数据类型,即使用如下代码即可获得指定成员变量的类型。

1
2
//获取成员变量field的类型
Class<?> a =field.getType();

但这种方式只对普通类型的成员变量有效。如果该成员变量的类型是有泛型类型的类型,如Map<String,Integer>类型,则不能准确地得到该成员变量的泛型参数。
为了获得指定成员变量的泛型类型,应先使用如下方法来获取该成员变量的泛型类型

1
2
//获得成员变量field的泛型类型
Type gType =field.getGenericType();

然后将Type对象强制类型转换为ParameterizedType对象,ParameterizedType代表被参数化的类型,也就是增加了泛型限制的类型

ParameterizedType类方法

方法 描述
Type[] getActualTypeArguments() 返回泛型参数的类型
Type getRawType() 返回没有泛型信息的原始类型。
Type getOwnerType() Returns a Type object representing the type that this type is a member of.

程序 获取成员变量的类型的泛型信息

下面是一个获取泛型类型的完整程序。

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
import java.util.*;
import java.lang.reflect.*;

public class GenericTest {
private Map<String, Integer> score;

public static void main(String[] args) throws Exception {
Class<GenericTest> clazz = GenericTest.class;
Field f = clazz.getDeclaredField("score");
// 直接使用getType()取出的类型只对普通类型的成员变量有效
Class<?> a = f.getType();
// 下面将看到仅输出java.util.Map
System.out.println("score的类型是:" + a);
// 获得成员变量f的泛型类型
Type gType = f.getGenericType();
// 如果gType类型是ParameterizedType对象
if (gType instanceof ParameterizedType) {
// 强制类型转换
ParameterizedType pType = (ParameterizedType) gType;
// 获取原始类型
Type rType = pType.getRawType();
System.out.println("原始类型是:" + rType);
// 取得泛型类型的泛型参数
Type[] tArgs = pType.getActualTypeArguments();
System.out.println("泛型信息是:");
for (int i = 0; i < tArgs.length; i++) {
System.out.println("第" + i + "个泛型类型是:" + tArgs[i]);
}
} else {
System.out.println("获取泛型类型出错!");
}
}
}

运行上面程序,将看到如下运行结果:

1
2
3
4
5
score的类型是:interface java.util.Map
原始类型是:interface java.util.Map
泛型信息是:
第0个泛型类型是:class java.lang.String
第1个泛型类型是:class java.lang.Integer

从上面的运行结果可以看出:

  • 使用getType()方法只能获取普通类型的成员变量的数据类型;
  • 对于增加了泛型的成员变量,应该使用getGenericType()方法来取得其类型。

Type接口

Type也是java.langreflect包下的一个接口,该接口代表所有类型的公共高级接口,ClassType接口的实现类。Type包括原始类型、参数化类型、数组类型、类型变量和基本类型等。

18.5.2 动态代理和AOP

开发实际应用的软件系统时,通常会存在相同代码段重复出现的情况,对于这种情况

  • 初级的开发只会复制粘贴这些代码.
  • 稍有经验的开发者会将这些重复的代码封装一个方法,然后直接调用该方法即可。但采用这种方式来实现代码复用依然产生一个重要问题:那就是调用者和这个方法耦合了。

最理想的情况是即可以执行这个相同的代码段,又无须调用封装的方法,这时就可以通过动态代理来达到这种效果。
由于**JDK动态代理只能为接口创建动态代理**,所以下面先提供一个Dog接口,该接口代码非常简单,仅仅在该接口里定义了两个方法。

1
2
3
4
5
6
7
public interface Dog
{
// info方法声明
void info();
// run方法声明
void run();
}

上面接口里只是简单地定义了两个方法,并未提供方法实现。如果直接使用Poxy为该接口创建动态代理对象,则动态代理对象的所有方法的执行效果又将完全一样。实际情况通常是,软件系统会为该Dog接口提供一个或多个实现类。此处先提供一个简单的实现类: GunDog

1
2
3
4
5
6
7
8
9
10
11
12
13
public class GunDog implements Dog
{
// 实现info()方法,仅仅打印一个字符串
public void info()
{
System.out.println("我是一只猎狗");
}
// 实现run()方法,仅仅打印一个字符串
public void run()
{
System.out.println("我奔跑迅速");
}
}

上面代码没有丝毫的特别之处,该Dog的实现类仅仅为每个方法提供了一个简单实现。
下面提供一个DogUtil类,该类里包含两个通用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class DogUtil
{
// 第一个拦截器方法
public void method1()
{
System.out.println("=====模拟第一个通用方法=====");
}
// 第二个拦截器方法
public void method2()
{
System.out.println("=====模拟通用方法二=====");
}
}

借助于ProxyInvocationHandler就可以实现——当程序调用info()方法和run()方法时,系统可以自动”将method1()method2()两个通用方法插入info()run()方法中执行。
这个程序的关键在于下面的MylnvokationHandler类,该类是一个InvocationHandler实现类,该实现类的invoke()方法将会作为代理对象的方法实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.lang.reflect.*;

public class MyInvokationHandler implements InvocationHandler
{
// 需要被代理的对象
private Object target;
public void setTarget(Object target)
{
this.target = target;
}
// 执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
public Object invoke(Object proxy, Method method, Object[] args)
throws Exception
{
DogUtil du = new DogUtil();
// 执行DogUtil对象中的method1。
du.method1();
// 以target作为主调来执行method方法
Object result = method.invoke(target , args);//关键代码
// 执行DogUtil对象中的method2。
du.method2();
return result;
}
}

上面程序实现invoke()方法时包含了一行关键代码,这行代码通过反射以target作为主调来执行method方法,这就是回调了target对象的原有方法。在粗体字代码之前调用DogUtil对象的method1()方法,在粗体字代码之后调用DogUti对象的method2()方法。
下面再为程序提供一个MyProxyFactory类,该类对象专为指定的target生成动态代理实例。

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

public class MyProxyFactory
{
// 为指定target生成动态代理对象
public static Object getProxy(Object target)
throws Exception
{
// 创建一个MyInvokationHandler对象
MyInvokationHandler handler =new MyInvokationHandler();
// 为MyInvokationHandler设置target对象
handler.setTarget(target);
// 创建、并返回一个动态代理
return Proxy.newProxyInstance(target.getClass().getClassLoader()
, target.getClass().getInterfaces() , handler);
}
}

上面的动态代理工厂类提供了一个getProxy()方法,该方法为target对象生成一个动态代理对象,这个动态代理对象与target实现了相同的接口,所以具有相同的public方法——一从这个意义上来看,动态代理对象可以当成target对象使用。当程序调用动态代理对象的指定方法时,实际上将变为执行MylnvokationHandler对象的invoke方法。例如,调用动态代理对象的info方法,程序将开始执行invoke方法,其执行步骤如下。
下面提供一个主程序来测试这种动态代理的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test
{
public static void main(String[] args)
throws Exception
{
// 创建一个原始的GunDog对象,作为target
Dog target = new GunDog();
// 以指定的target来创建动态代理
Dog dog = (Dog)MyProxyFactory.getProxy(target);
dog.info();
dog.run();
}
}

上面程序中的dog对象实际上是动态代理对象,只是该动态代理对象也实现了Dog接口,所以也可以当成Dog对象使用。程序执行doginfo()run()方法时,实际上会先执行DogUtilmethod1()方法,再执行target对象的info()run()方法,最后执行DogUtilmethod2()方法。运行结果如下:

1
2
3
4
5
6
=====模拟第一个通用方法=====
我是一只猎狗
=====模拟通用方法二=====
=====模拟第一个通用方法=====
我奔跑迅速
=====模拟通用方法二=====

采用动态代理可以非常灵活地实现解耦。通常而言,使用Proxy生成一个动态代理时,往往并不会凭空产生一个动态代理,这样没有太大的实际意义。通常都是为指定的目标对象生成动态代理
这种动态代理在AOP( Aspect Orient Programming,面向切面编程)中被称为AOP代理,AOP代理可代替目标对象,AOP代理包含了目标对象的全部方法。但AOP代理中的方法与目标对象的方法存在差异:AOP代理里的方法可以在执行目标方法之前、之后插入一些通用处理。

本文重点

JDK动态代理只能为接口创建动态代理
创建代理对象的步骤:

  1. 编写自定义InvocationHandler实现类,重写invoke()方法,nvoke方法的第一个参数proxy代表要动态代理的对象,第二个参数method:代表要执行的目标方法第三个参数args:代表调用目标方法时传入的实参。在invoke()方法中,在目标方法的前面和后面添加增强的方法.
  2. 编写接口,以及该接口的实现类,然后创建实现类,赋值给接口引用(多态),这样就得到了一个接口的实例对象(目标对象)
  3. 调用Proxy.newProxyInstance()方法生成代理对象,newProxyInstance方法的第一个参数是要代理的目标对象的类加载器,第二个参数是目标对象的接口,第三个参数是自定义InvocationHandler实现类。
  4. 由于代理类和被代理类都实现相同的接口,所以可以调用同名的方法.

18.5.1使用Proxy和InvocationHandler创建动态代理

Javajava.lang.reflect包下提供了一个Proxy类和一个InvocationHandler接口,通过使用这个类和接口可以生成JDK动态代理类或动态代理对象
Proxy提供了用于创建动态代理类代理对象的静态方法,它也是所有动态代理类的父类

  • 如果在程序中为一个或多个接口动态地生成实现类,就可以使用Proxy来创建动态代理类;
  • 如果需要为一个或多个接口动态地创建实例,也可以使用Proxy来创建动态代理实例。

创建动态代理类和动态代理实例的方法

Proxy提供了如下两个方法来创建动态代理类和动态代理实例。

方法 描述
static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) 创建一个动态代理类所对应的 Class对象,该代理类将实现 interfaces所指定的多个接口。第一个 ClassLoader参数指定生成动态代理类的类加载器。
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 直接创建一个动态代理对象,该代理对象的实现类实现了 interfaces指定的系列接口,执行代理对象的每个方法时都会被替换执行 Invocation Handler对象的 invoke方法

每个代理对象都有一个与之关联的InvocationHandler对象

实际上,即使采用第一个方法生成动态代理类之后,如果程序需要通过该代理类来创建对象,依然需要传入一个InvocationHandler对象。也就是说,系统生成的每个代理对象都有一个与之关联的InvocationHandler对象

程序中可以采用先生成一个动态代理类,然后通过动态代理类来创建代理对象的方式生成一个动态代理对象。代码片段如下:

1
2
3
4
// 创建一个InvocationHandler对象
InvocationHandler handler = new MyInvokationHandler();
// 使用指定的InvocationHandler来生成一个动态代理对象
Person p = (Person)Proxy.newProxyInstance(Person.class.getClassLoader(),new Class[]{Person.class}, handler);

下面程序示范了使用ProxyInvocationHandler来生成动态代理对象。

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
import java.lang.reflect.*;

interface Person
{
void walk();
void sayHello(String name);
}
class MyInvokationHandler implements InvocationHandler
{
/*
执行动态代理对象的所有方法时,都会被替换成执行如下的invoke方法
其中:
:代表动态代理对象
method:代表正在执行的方法
args:代表调用目标方法时传入的实参。
*/
public Object invoke(Object proxy, Method method, Object[] args)
{
System.out.println("----正在执行的方法:" + method);
if (args != null)
{
System.out.println("下面是执行该方法时传入的实参为:");
for (Object val : args)
{
System.out.println(val);
}
}
else
{
System.out.println("调用该方法没有实参!");
}
return null;
}
}
public class ProxyTest
{
public static void main(String[] args)
throws Exception
{
// 创建一个InvocationHandler对象
InvocationHandler handler = new MyInvokationHandler();
// 使用指定的InvocationHandler来生成一个动态代理对象
Person p = (Person)Proxy.newProxyInstance(Person.class.getClassLoader(),new Class[]{Person.class}, handler);
// 调用动态代理对象的walk()和sayHello()方法
p.walk();
p.sayHello("孙悟空");
}
}

上面程序首先提供了一个Person接口,该接口中包含了walk()sayHello()两个抽象方法,接着定义了一个简单的InvocationHandler实现类,定义该实现类时需要重写invoke()方法——调用代理对象的所有方法时都会被替换成调用该invoke()方法。该invoked方法中的三个参数解释如下:

  • proxy:代表动态代理对象。
  • method:代表正在执行的方法。
  • args:代表调用目标方法时传入的实参。

上面程序中第一行粗体字代码创建了一个InvocationHandler对象,第二行粗体字代码根据InvocationHandler对象创建了一个动态代理对象。运行上面程序,效果如下:

1
2
3
4
5
----正在执行的方法:public abstract void Person.walk()
调用该方法没有实参!
----正在执行的方法:public abstract void Person.sayHello(java.lang.String)
下面是执行该方法时传入的实参为:
孙悟空

不管程序是执行代理对象的walk()方法,还是执行代理对象的sayhello()方法,实际上都是执行InvocationHandler对象的invoked方法。

本文重点

执行代理对象的方法,实际上是执行InvocationHandler对象的invoke()方法。

18.4 使用反射生成并操作对象 18.4.3 访问成员变量值

通过Class对象的getFields()getField()方法可以获取该类所包括的全部成员变量或指定成员变量(Field对象).

Field类

Field提供了如下两组方法来读取或设置指定成员变量的值。

访问基本类型的成员变量

  • getXxx(Object object):获取object对象的该成员变量的值。此处的Xxx对应8种基本类型,
  • setXxx(Object object,Xxx value):将object对象的该成员变量设置成value值。此处的Xxx对应8种基本类型,

访问引用类型的成员变量

对应引用类型,直接使用getset方法即可

  • get(Object object):获取object对象的该成员变量的值。
  • set(Object object,Xxx value):将object对象的该成员变量设置成value值。

使用这两个方法可以随意地访问指定对象的所有成员变量,包括private修饰的成员变量。

程序示例

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
import java.lang.reflect.*;
class Person
{
//私有的成员变量
private String name;
//私有的成员变量
private int age;
public String toString()
{
return "Person[name:" + name +
" , age:" + age + " ]";
}
}
public class FieldTest
{
public static void main(String[] args)
throws Exception
{
// 创建一个Person对象
Person p = new Person();
// 获取Person类对应的Class对象
Class<Person> personClazz = Person.class;
// 获取Person的名为name的成员变量
// 使用getDeclaredField()方法表明可获取各种访问控制符的成员变量
Field nameField = personClazz.getDeclaredField("name");
// 设置通过反射访问 name 成员变量时 取消访问权限检查
nameField.setAccessible(true);
// 调用set()方法为p对象的name成员变量设置值
nameField.set(p , "小明");

// 获取Person类名为age的成员变量
Field ageField = personClazz.getDeclaredField("age");
// 通过反射访问 age 成员变量时 取消访问权限检查
// (要在getter和setter方法之前设置)
ageField.setAccessible(true);
// 调用setInt()方法为p对象的age成员变量设置值
ageField.setInt(p , 30);
// 获取p对象的age成员变量的值
System.out.println("p对象的age成员变量的值:"+ageField.getInt(p));
System.out.println(p);
}
}

运行结果:

1
2
p对象的age成员变量的值:30
Person[name:小明 , age:30 ]

代码详解

上面程序中先定义了一个Person类,该类里包含两个private成员变量:nameage,在通常情况下,这两个成员变量只能在Person类里访问。但本程序FieldTestmain()方法中通过反射修改了Person对象的nameage两个成员变量的值。

  • 代码:personClazz.getDeclaredField("name");中的getDeclaredField方法获取了名为name的成员变量,注意此处不是使用getField方法,因为**getField方法只能获取public访问控制的成员变量,而getDeclaredField方法则可以获取所有的成员变量**:
  • 代码:nameField.setAccessible(true);设置通过反射访问成员变量name时不受访问权限的控制;
  • 代码nameField.set(p , "Yeeku.H.Lee");修改了Person对象的name成员变量的值。

修改Person对象的age成员变量的值的方式与此完全相同。
编译、运行上面程序,会看到如下输出:

1
Person[name:Yeeku.H.Lee , age:30 ]

总结

Class对象的**getField方法只能获取public访问控制的成员变量,而getDeclaredField方法则可以获取所有的成员变量**在获取(get)或者设置(set)某个私有的成员变量之前,必须先调用该成员变量(Field)setAccessible方法,并把参数设置为true.
例如要获取成员变量name的值之前,要先调用:nameField.setAccessible(true);`

18.4.4 操作数组

java.lang.reflect包下还提供了一个Array类,Array对象可以代表所有的数组。

Array有什么有用

程序可以通过使用Array来动态地创建数组,操作数组元素等.

Array类方法

Array提供了如下几类方法。

方法 描述
static Object newInstance(Class<?> componentType,int ... length) 创建一个具有指定的元素类型、指定维度的新数组.
static Object get(Object array, int index) 返回array数组中第index个元素, 类型为Object
static void set(Object array, int index, Object value) array数组中第index个元素的值设为value
static Xxx getXxx(Object array, int index) 返回array数组中第index个元素,其中Xxx指的是各种基本数据类型。
static void setXxx(Object array, int index,Xxx value) array数组中第index个元素的值设为value,其中Xxx指的是各种基本数据类型

程序示例 Array创建一维数组

下面程序示范了如何使用Array来生成数组,为指定数组元素赋值,并获取指定数组元素的方式。

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
import java.lang.reflect.*;

public class ArrayTest1
{
public static void main(String args[])
{
try
{
// 创建一个元素类型为String ,长度为10的数组
Object arr = Array.newInstance(String.class, 10);
// 依次为arr数组中index为5、6的元素赋值
Array.set(arr, 5, "这是一个字符串");
Array.set(arr, 6, "这还是一个字符串");
// 依次取出arr数组中index为5、6的元素的值
Object book1 = Array.get(arr , 5);
Object book2 = Array.get(arr , 6);
// 输出arr数组中index为5、6的元素
System.out.println(book1);
System.out.println(book2);
}
catch (Throwable e)
{
System.err.println(e);
}
}
}

运行结果:

1
2
这是一个字符串
这还是一个字符串

上面程序中分别通过Array创建数组,为数组元素设置值,访问数组元素的值的示例代码,程序通过使用Array就可以动态地创建并操作数组。

程序示例 Array创建多维数组

下面程序比上面程序稍微复杂一点,下面程序使用Array类创建了一个三维数组

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
import java.lang.reflect.*;

public class ArrayTest2
{
public static void main(String args[])
{
/*
创建一个三维数组。
根据前面介绍数组时讲的:三维数组也是一维数组,
是数组元素是二维数组的一维数组,
因此可以认为arr是长度为3的一维数组
*/
Object arr = Array.newInstance(String.class, 3, 4, 10);//代码段1
// 获取三维数组arr中index为2的元素,该元素应该是二维数组
Object arrObj = Array.get(arr, 2);
// 使用Array为二维数组的数组元素赋值。二维数组的数组元素是一维数组,
// 所以传入Array的set()方法的第三个参数是一维数组。
// 代码段2
Array.set(arrObj , 2 , new String[]
{
"这是一个字符串",
"这又是一个字符串"
});

// 获取二维数组arrObj中index为3的元素,该元素应该是一维数组。
Object anArr = Array.get(arrObj, 3);
Array.set(anArr , 8 , "这还是一个字符串");
// 将arr强制类型转换为三维数组
String[][][] cast = (String[][][])arr;
// 获取cast三维数组中指定元素的值
System.out.println(cast[2][3][8]);
System.out.println(cast[2][2][0]);
System.out.println(cast[2][2][1]);
}
}

上面程序的代码段1使用Array创建了一个三维数组,程序中较难理解的地方是代码段2这部分,使用ArrayarrObj的指定元素赋值,相当于为二维数组的元素赋值。由于二维数组的元素是一维数组,所以程序传入的参数是一个一维数组对象。
运行上面程序,将看到cast[2][3][8]cast[2][2][0]cast[2][2][1]]元素都有值,这些值就是刚才程序通过反射传入的数组元素值。

本文重点

  • 使用ArraynewInstance这个静态方法可以创建数组,
  • 该方法的第一个参数,是数组的类型,
    • 第二分变长参数的个数表示数组的维度,
      • 如果有一个变长参数,表示创建一维数组,
      • 如果有多个变长参数,则表示创建多维数组.
    • 变长参数的大小表示数组的长度.
  • Array方法提供getset方法通过下标访问数组元素,但是要注意的是多维数组的情况:
    • 由于二维数组的元素是一维数组,则此时的通过下标来访问的get,或者set方法,访问到的是一维数组.
    • 由于三维数组的元素时二维数组,则测试通过访问的get,set方法访问到的是二维数组.

18.3.3 Java 8新增的方法参数反射

Java8java.lang.reflect包下新增了一个Executable抽象基类,Executable类对象代表可执行的类成员,该类派生了ConstructorMethod两个子类。

获取方法或构造器的形参个数 形参名

Executable基类提供了大量方法来获取修饰该方法构造器的注解信息;还提供了isVarArgs()方法用于判断该方法或构造器是否包含数量可变的形参,以及通过getModifiers()方法来获取该方法或构造器的修饰符。除此之外, Executable提供了如下两个方法来获取该方法或构造器的形参个数形参名

方法 描述
int getParameter() 获取该构造器或方法的形参个数
Parameter[] getParameters() 获取该构造器或方法的所有形参。

获取参数信息

上面第二个方法返回了一个Parameter数组, Parameter也是Java 8新增的APl,每个Parameter对象代表方法或构造器的一个参数。 Parameter也提供了大量方法来获取声明该参数的泛型信息,还提供了如下常用方法来获取参数信息。

方法 描述
getModifiers() 获取修饰该形参的修饰符。
String getName() 获取形参名
Type getParameterizedType() 获取带泛型的形参类型。
Cass<?> getType() 获取形参类型
boolean isNamePresent() 该方法返回该类的class件中是否包含了方法的形参名信息。
boolean isVarArgs() 该方法用于判断该参数是否为个数可变的形参。

需要指出的是,使用javac命令编译Java源文件时,默认生成的.class文件并不包含方法的形参名信息,因此调用isNamePresent()方法将会返回false,调用getName()方法也不能得到该参数的形参名。如果希望javac命令编译Java源文件时可以保留形参信息,则需要为该命令指定-parameters选项。

程序示例

如下程序示范了Java 8的方法参数反射功能。

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
import java.lang.reflect.*;
import java.util.*;

class Test
{
public void replace(String str, List<String> list){}
}
public class MethodParameterTest
{
public static void main(String[] args)throws Exception
{
// 获取String的类
Class<Test> clazz = Test.class;
// 获取String类的带两个参数的replace()方法
Method replace = clazz.getMethod("replace"
, String.class, List.class);
// 获取指定方法的参数个数
System.out.println("replace方法参数个数:" + replace.getParameterCount());
// 获取replace的所有参数信息
Parameter[] parameters = replace.getParameters();
int index = 1;
// 遍历所有参数
for (Parameter p : parameters)
{
if (p.isNamePresent())
{
System.out.println("---第" + index++ + "个参数信息---");
System.out.println("参数名:" + p.getName());
System.out.println("形参类型:" + p.getType());
System.out.println("泛型类型:" + p.getParameterizedType());
}
}
}
}

上面程序先定义了一个包含简单的Test类,该类中包含一个replace(String str,List<String> list)方法,程序中先是获取了该方法,然后分别用于获取该方法的形参名,形参类型和泛型信息。
只有当该类的.class文件中包含形参名信息时,程序才会执行条件体内的三行粗体字代码。因此需要使用如下命令来编译该程序:
javac -parameters MethodParameterTest.java
上面命令中-parameters选项用于控制javac命令保留方法形参名信息。
运行该程序,即可看到如下输出:

1
2
3
4
5
6
7
8
9
10
11
12
G:\Desktop\随书源码\疯狂Java讲义第三版光盘\codes\18\18.3>javac -parameters MethodParameterTest.java

G:\Desktop\随书源码\疯狂Java讲义第三版光盘\codes\18\18.3>java MethodParameterTest
replace方法参数个数:2
---第1个参数信息---
参数名:str
形参类型:class java.lang.String
泛型类型:class java.lang.String
---第2个参数信息---
参数名:list
形参类型:interface java.util.List
泛型类型:java.util.List<java.lang.String>

本文重点

Java 8新增了方法参数反射功能,可以获取方法的参数的信息,如获取:

  • 形参的修饰符
  • 形参名
  • 形参个数等信息

要想保留形参名信息到.class文件中,则在编译时要在javac命令后面加上-parameters选项.

12.10.2 拖动 编辑树节点

JTree默认不可编辑

JTree生成的树默认是不可编辑的,不可以添加、删除节点,也不可以改变节点数据;
如果想让某个JTree对象变成可编辑状态,则可以调用JTreesetEditable(boolean b)方法,传入true即可把这棵树变成可编辑的树(可以添加、删除节点,也可以改变节点数据)。

方法 描述
void setEditable(boolean flag) Determines whether the tree is editable.

一旦将JTree对象设置成可编辑状态后,程序就可以为指定节点添加子节点、兄弟节点,也可以修改、删除指定节点。
前面简单提到过,JTree处理节点有两种方式:

  • 一种是根据TreePath;
  • 另一种是根据节点的行号.

所有JTree显示的节点都有一个唯一的行号(从0开始)。只有那些被显示出来的节点才有行号,这就带来一个潜在的问题:
如果该节点之前的节点被展开、折叠或增加、删除后,那么该节点的行号就会发生变化,因此通过行号来识别节点可能有一些不确定的地方;相反,使用TreePath来识别节点则会更加稳定。

可以使用文件系统来类比JTree,从图12.38中可以看出,实际上所有的文件系统都采用树状结构,其中

  • Windows的文件系统是森林,因为Windows包含C、D等多个根路径,
  • UNIXLinux的文件系统是一棵树,只有一个根路径。

如果直接给岀abc文件夹(类似于JTree中的节点),系统不能准确地定位该路径;
如果给出D:\xyz\abc,系统就可以准确地定位到该路径,这个D:\xyz\abc实际上由三个文件夹组成:D:xyzabc,其中D:是该路径的根路径。类似地,TreePath也采用这种方式来唯地标识节点。

TreePath保持着从根节点到指定节点的所有节点,**TreePath由一系列节点组成**,而不是单独的一个节点。

获取被选中的节点

JTree的很多方法都用于返回一个TreePath对象,当程序得到一个TreePath后,可以调用TreePathgetLastPathComponent​方法获取最后一个节点

例如:需要获得JTree中被选定的节点,则可以通过如下两行代码来实现。

1
2
3
4
//获取选中节点所在的 Treepath
TreePath path = tree.getselectionPath();
//获取指定 Treepath的最后一个节点
TreeNode target = (TreeNode) path.getLastPathcomponent();
TreePath方法 描述
Object getLastPathComponent() Returns the last element of this path.

获取选中的节点

又因为JTree经常需要查询被选中的节点,所以JTree提供了一个getLastSelectedPathComponent方法来获取选中的节点。
比如采用下面代码也可以获取选中的节点。

JTree方法 描述
Object getLastSelectedPathComponent() Returns the last path component of the selected path.
1
2
//获取选中的节点
TreeNode target =(TreeNode) tree.getLastselectedPathComponent();

可能有读者对上面这行代码感到奇怪,getLastSelectedPathComponent()方法返回的不是TreeNode吗?getLastSelectedPathComponent方法返回的不一定是TreeNode,该方法的返回值是Object。因为SwingJTree设计得非常复杂,JTree把所有的状态数据都交给TreeModel管理,而JTree本身并没有与TreeNode发生关联(从图12.40可以看出这一点),只是因为DefaultTreeModel需要TreeNode而已,如果开发者自己提供一个TreeModel实现类,这个TreeModel实现类完全可以与TreeNode没有任何关系。当然,对于大部分Swing开发者而言,无须理会JTree的这些过于复杂的设计。

TreeNode转成TreePath

如果已经有了从根节点到当前节点的一系列节点所组成的节点数组,也可以通过TreePath提供的构造器将这些节点转换成TreePath对象,如下代码所示。

1
2
//将一个节点数组转换成 Treepath对象
TreePath tp = new TreePath(nodes);

获取了选中的节点之后,即可通过DefaultTreeModel(它是SwingTreeModel提供的唯一一个实现类)提供的系列方法来插入、删除节点。DefaultTreeModel类有一个非常优秀的设计,当使用DefaultTreeModel插入、删除节点后,该DefaultTreeModel会自动通知对应的JTree重绘所有节点,用户可以立即看到程序所做的修改

也可以直接通过TreeNode提供的方法来添加、删除和修改节点。

但通过TreeNode改变节点时,程序必须显式调用JTreeupdateUI()通知JTree重绘所有节点,让用户看到程序所做的修改。

程序 增加 修改 删除节点 拖动节点

下面程序实现了增加、修改和删除节点的功能,并允许用户通过拖动将一个节点变成另一个节点的子节点。

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
import java.awt.BorderLayout;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.tree.*;

public class EditJTree {
JFrame jf;
JTree tree;
// 上面JTree对象对应的model
DefaultTreeModel model;
// 定义几个初始节点
DefaultMutableTreeNode root = new DefaultMutableTreeNode("中国");
DefaultMutableTreeNode guangdong = new DefaultMutableTreeNode("广东");
DefaultMutableTreeNode guangxi = new DefaultMutableTreeNode("广西");
DefaultMutableTreeNode foshan = new DefaultMutableTreeNode("佛山");
DefaultMutableTreeNode shantou = new DefaultMutableTreeNode("汕头");
DefaultMutableTreeNode guilin = new DefaultMutableTreeNode("桂林");
DefaultMutableTreeNode nanning = new DefaultMutableTreeNode("南宁");
// 定义需要被拖动的TreePath
TreePath movePath;
JButton addSiblingButton = new JButton("添加兄弟节点");
JButton addChildButton = new JButton("添加子节点");
JButton deleteButton = new JButton("删除节点");
JButton editButton = new JButton("编辑当前节点");

public void init() {
guangdong.add(foshan);
guangdong.add(shantou);
guangxi.add(guilin);
guangxi.add(nanning);
root.add(guangdong);
root.add(guangxi);
jf = new JFrame("可编辑节点的树");
tree = new JTree(root);
// 获取JTree对应的TreeModel对象
model = (DefaultTreeModel) tree.getModel();
// 设置JTree可编辑
tree.setEditable(true);
MouseListener ml = new MouseAdapter() {
// 按下鼠标时获得被拖动的节点
public void mousePressed(MouseEvent e) {
// 如果需要唯一确定某个节点,必须通过TreePath来获取。
TreePath tp = tree.getPathForLocation(e.getX(), e.getY());
if (tp != null) {
movePath = tp;
}
}

// 鼠标松开时获得需要拖到哪个父节点
public void mouseReleased(MouseEvent e) {
// 根据鼠标松开时的TreePath来获取TreePath
TreePath tp = tree.getPathForLocation(e.getX(), e.getY());
if (tp != null && movePath != null) {
// 阻止向子节点拖动
if (movePath.isDescendant(tp) && movePath != tp) {
JOptionPane.showMessageDialog(jf, "目标节点是被移动节点的子节点,无法移动!", "非法操作", JOptionPane.ERROR_MESSAGE);
return;
}
// 既不是向子节点移动,而且鼠标按下、松开的不是同一个节点
else if (movePath != tp) {
// add方法先将该节点从原父节下删除,再添加到新父节点下
((DefaultMutableTreeNode) tp.getLastPathComponent())
.add((DefaultMutableTreeNode) movePath.getLastPathComponent());
movePath = null;
tree.updateUI();
}
}
}
};
// 为JTree添加鼠标监听器
tree.addMouseListener(ml);
JPanel panel = new JPanel();
// 实现添加兄弟节点的监听器
addSiblingButton.addActionListener(event -> {
// 获取选中节点
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
// 如果节点为空,直接返回
if (selectedNode == null)
return;
// 获取该选中节点的父节点
DefaultMutableTreeNode parent = (DefaultMutableTreeNode) selectedNode.getParent();
// 如果父节点为空,直接返回
if (parent == null)
return;
// 创建一个新节点
DefaultMutableTreeNode newNode = new DefaultMutableTreeNode("新节点");
// 获取选中节点的选中索引
int selectedIndex = parent.getIndex(selectedNode);
// 在选中位置插入新节点
model.insertNodeInto(newNode, parent, selectedIndex + 1);
// --------下面代码实现显示新节点(自动展开父节点)-------
// 获取从根节点到新节点的所有节点
TreeNode[] nodes = model.getPathToRoot(newNode);
// 使用指定的节点数组来创建TreePath
TreePath path = new TreePath(nodes);
// 显示指定TreePath
tree.scrollPathToVisible(path);
});
panel.add(addSiblingButton);
// 实现添加子节点的监听器
addChildButton.addActionListener(event -> {
// 获取选中节点
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
// 如果节点为空,直接返回
if (selectedNode == null)
return;
// 创建一个新节点
DefaultMutableTreeNode newNode = new DefaultMutableTreeNode("新节点");
// 通过model来添加新节点,则无须通过调用JTree的updateUI方法
// model.insertNodeInto(newNode, selectedNode
// , selectedNode.getChildCount());
// 通过节点添加新节点,则需要调用tree的updateUI方法
selectedNode.add(newNode);
// --------下面代码实现显示新节点(自动展开父节点)-------
TreeNode[] nodes = model.getPathToRoot(newNode);
TreePath path = new TreePath(nodes);
tree.scrollPathToVisible(path);
tree.updateUI();
});
panel.add(addChildButton);
// 实现删除节点的监听器
deleteButton.addActionListener(event -> {
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
if (selectedNode != null && selectedNode.getParent() != null) {
// 删除指定节点
model.removeNodeFromParent(selectedNode);
}
});
panel.add(deleteButton);
// 实现编辑节点的监听器
editButton.addActionListener(event -> {
TreePath selectedPath = tree.getSelectionPath();
if (selectedPath != null) {
// 编辑选中节点
tree.startEditingAtPath(selectedPath);
}
});
panel.add(editButton);
jf.add(new JScrollPane(tree));
jf.add(panel, BorderLayout.SOUTH);
jf.pack();
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}

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

上面程序中实现拖动节点也比较容易:
当用户按下鼠标时获取鼠标事件发生位置的树节点,并把该节点赋给movePath变量;
当用户松开鼠标时获取鼠标事件发生位置的树节点,作为目标节点需要拖到的父节点,把movePath从原来的节点中删除,添加到新的父节点中即可(TreeNodeadd()方法可以同时完成这两个操作)。

运行上面程序,会看到如图12.42所示的效果。


选中图12.42中的某个节点并双击,或者单击“编辑当前节点”按钮,就可以进入该节点的编辑状态,系统启动默认的单元格编辑器来编辑该节点,JTree的单元格编辑器与JTable的单元格编辑器都实现了相同的CellEditor接口。

12.10 使用JTree和TreeModel创建树

树也是图形用户界面中使用非常广泛的GUI组件,例如使用Windows资源管理器时,将看到如图12.38所示的目录树。

如图12.38所示的树,代表计算机世界里的树,它从自然界实际的树抽象而来。计算机世界里的树是由一系列具有严格父子关系的节点组成的,每个节点既可以是其上一级节点的子节点,也可以是其下一级节点的父节点,因此同一个节点既可以是父节点,也可以是子节点(类似于一个人,他既是他儿子的父亲,又是他父亲的儿子)。

节点分类

如果按节点是否包含子节点来分,节点分为如下两种

  • 普通节点:包含子节点的节点。
  • 叶子节点:没有子节点的节点,因此叶子节点不可作为父节点。

如果按节点是否具有唯一的父节点来分,节点又可分为如下两种

  • 根节点:没有父节点的节点,根节点不可作为子节点。
  • 普通节点:具有唯一父节点的节点。

棵树只能有一个根节点,如果一棵树有了多个根节点,那它就不是一棵树了,而是多棵树的集合,有时也被称为森林。图12.39显示了计算机世界里树的一些专业术语。
使用Swing里的JTreeTreeModel及其相关的辅助类可以很轻松地开发出计算机世界里的树,如图1239所示

12.10.1 创建树

Swing使用JTree对象来代表一棵树(实际上,JTree可以代表森林,因为在使用JTree创建树时可以传入多个根节点),
JTree树中节点可以使用TreePath来标识,**TreePath对象封装了当前节点及其所有的父节点。必须指出,节点及其所有的父节点才能唯一地标识一个节点**;也可以使用行数来标识,如图12.39所示,显示区域的每一行都标识一个节点。
当一个节点具有子节点时,该节点有两种状态。

  • 展开状态:当父节点处于展开状态时,其子节点是可见的。
  • 折叠状态:当父节点处于折叠状态时,其子节点都是不可见的。

如果某个节点是可见的,则该节点的父节点(包括直接的、间接的父节点)都必须处于展开状态,只要有任意一个父节点处于折叠状态,该节点就是不可见的。
如果希望创建一棵树,则直接使用JTree的构造器创建JTree对象即可。JTree提供了如下几个常用构造器。

方法 描述
JTree(TreeModel newModel) 使用指定的数据模型创建JTree对象,它默认显示根节点。
JTree(TreeNode root) 使用root作为根节点创建JTree对象,它默认显示根节点。
JTree(TreeNode root, boolean asksAllowsChildren) 使用root作为根节点创建JTree对象,它默认显示根节点。**asksAllowsChildren参数控制怎样的节点才算叶子节点**:
  • 如果该参数为true,则只有当程序使用setAllowsChildren(false)显式设置某个节点不允许添加子节点时(以后也不会拥有子节点),该节点才会被JTree当成叶子节点;
  • 如果该参数为false,则只要某个节点当时没有子节点(不管以后是否拥有子节点),该节点都会被JTree当成叶子节点。

上面的第一个构造器需要显式传入一个TreeModel对象,SwingTreeModel提供了一个DefaultTreeModel实现类,通常可先创建DefaultTreeModel对象,然后利用DefaultTreeModel来创建JTree,但通过DefaultTreeModelAPI文档会发现,创建DefaultTreeModel对象依然需要传入根节点,所以直接通过根节点创建JTree更加简洁。
为了利用根节点来创建JTree,程序需要创建一个TreeNode对象。TreeNode是一个接口,该接口有个MutableTreeNode子接口,Swing为该接口提供了默认的实现类:DefaultMutableTreeNode,程序可以通过DefaultMutableTreeNode来为树创建节点,并通过DefaultMutableTreeNode提供的add()方法建立各节点之间的父子关系,然后调用JTreeJTree(TreeNodeRoot构造器来创建一棵树。

图12.40显示了JTree相关类的关系,从该图可以看出DefaultTreeModelTreeModel的默认实现类,当程序通过TreeNode类创建JTree时,其状态数据实际上由DefaultTreeModel对象维护,因为创建JTree时传入的TreeNode对象,实际上传给了DefaultTreeModel对象。

DefaultTreeModel也提供了DefaultTreeModel(TreeNode root)构造器,用于接收一个TreeNode根节点来创建一个默认的TreeModel对象;当程序中通过传入一个根节点来创建JTree对象时,实际上是将该节点传入对应的DefaultTreeModel对象,并使用该DefaultTreeModel对象来创建JTree对象

程序 简单Swing数组

下面程序创建了一棵最简单的Swing树:

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
import javax.swing.*;
import javax.swing.tree.*;

public class SimpleJTree {
JFrame jf = new JFrame("简单树");
JTree tree;
DefaultMutableTreeNode root;
DefaultMutableTreeNode guangdong;
DefaultMutableTreeNode guangxi;
DefaultMutableTreeNode foshan;
DefaultMutableTreeNode shantou;
DefaultMutableTreeNode guilin;
DefaultMutableTreeNode nanning;

public void init() {
// 依次创建树中所有节点
root = new DefaultMutableTreeNode("中国");
guangdong = new DefaultMutableTreeNode("广东");
guangxi = new DefaultMutableTreeNode("广西");
foshan = new DefaultMutableTreeNode("佛山");
shantou = new DefaultMutableTreeNode("汕头");
guilin = new DefaultMutableTreeNode("桂林");
nanning = new DefaultMutableTreeNode("南宁");
// 通过add()方法建立树节点之间的父子关系
guangdong.add(foshan);
guangdong.add(shantou);
guangxi.add(guilin);
guangxi.add(nanning);
root.add(guangdong);
root.add(guangxi);
// 以根节点创建树
tree = new JTree(root); // ①
jf.add(new JScrollPane(tree));
jf.pack();
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}

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

上面程序中的粗体字代码创建了一系列的DefaultMutableNode对象,并通过add()方法为这些节点建立了相应的父子关系。程序中①号代码则以一个根节点创建了一个JTree对象。当程序把JTree对象添加到其他容器中后,JTree就会在该容器中绘制出一棵Swing树。运行上面程序,会看到如图12.41所示的窗口。

从图12.41中可以看出,Swing树的默认风格是使用一个特殊图标来表示节点的展开、折叠,而不是使用我们熟悉的“+”、“-”图标来表示节点的展开、折叠。如果希望使用“+”、“-”图标来表示节点的展开、折叠,则可以考虑使用Windows风格。

不显示节点之间的连线

从图12.41中可以看出,Swing树默认使用连接线来连接所有节点,程序可以使用如下代码来强制JTree不显示节点之间的连接线。

1
2
//没有连接线
tree.putClientProperty("JTree.lineStyle","None");

只有水平分割线

或者使用如下代码来强制节点之间只有水平分隔线。
图12.41中显示的根节点前没有绘制表示节点展开、折叠的特殊图标,如果希望根节点也绘制表示节点展开、折叠的特殊图标,则使用如下代码。

1
2
//设置是否显示根节点的“展开、折叠”图标,默认是fase
tree.setShowsRootHandles(true);

隐藏根节点

JTree甚至允许把整个根节点都隐藏起来,可以通过如下代码来隐藏根节点。

1
2
//设置根节点是否可见,默认是true
tree.setRootvisible(false);

遍历所有子节点

DefaultMutableTreeNodeJTree默认的树节点,该类提供了大量的方法来访问树中的节点,包括遍历该节点的所有子节点的两个方法。

方法 描述
Enumeration<TreeNode> breadthFirstEnumeration() 按广度优先的顺序遍历以此节点为根的子树,并返回所有节点组成的枚举对象。
Enumeration<TreeNode> preorderEnumeration() Creates and returns an enumeration that traverses the subtree rooted at this node in preorder.
Enumeration<TreeNode> depthFirstEnumeration() 按深度优先的顺序遍历以此节点为根的子树,并返回所有节点组成的枚举对象。
Enumeration<TreeNode> postorderEnumeration() Creates and returns an enumeration that traverses the subtree rooted at this node in postorder.

获取节点

除此之外,DefaultMutableTreeNode也提供了大量的方法来获取指定节点的兄弟节点、父节点节点等,常用的有如下几个方法:

方法 描述
DefaultMutableTreeNode getNextSibling() 返回此节点的下一个兄弟节点
TreeNode getParent() 返回此节点的父节点。如果此节点没有父节点,则返回null
TreeNode[] getPath() 返回从根节点到达此节点的所有节点组成的数组。
protected TreeNode[] getPathToRoot(TreeNode aNode, int depth) 返回包含此节点的树的根节点。
DefaultMutableTreeNode getPreviousSibling() 返回此节点的上一个兄弟节点。
TreeNode getRoot() 返回包含此节点的树的根节点
TreeNode getSharedAncestor(DefaultMutableTreeNode aNode) 返回此节点和aNode最近的共同祖先
int getSiblingCount() 返回此节点的兄弟节点数
boolean isLeaf() 返回该节点是否是叶子节点。
boolean isNodeAncestor(TreeNode anotherNode) 判断anotherNode是否是当前节点的祖先节点(包括父节点)。
boolean isNodeChild(TreeNode aNode) 如果aNode是此节点的子节点,则返回true
boolean isNodeDescendant(DefaultMutableTreeNode anotherNode) 如果anotherNode是此节点的后代,包括是此节点本身、此节点的子节点或此节点的子节点的后代,都将返回tnue
boolean isNodeRelated(DefaultMutableTreeNode aNode) aNode和当前节点位于同一棵树中时返回true
boolean isNodeSibling(TreeNode anotherNode) 返回anotherNode是否是当前节点的兄弟节点。
boolean isRoot() 返回当前节点是否是根节点。
Enumeration<TreeNode> pathFromAncestorEnumeration(TreeNode ancestor) 返回从指定祖先节点到当前节点的所有节点组成的枚举对象