本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http://item.jd.com/12299018.html


上节,我们探讨了动态代理,在前几节中,我们多次提到了类加载器ClassLoader,本节就来详细讨论Java中的类加载机制与ClassLoader。

类加载器ClassLoader就是加载其他类的类,它负责将字节码文件加载到内存,创建Class对象。与之前介绍的反射注解、和动态代理一样,在大部分的应用编程中,我们不太需要自己实现ClassLoader。

不过,理解类加载的机制和过程,有助于我们更好的理解之前介绍的内容,更好的理解Java。在反射一节,我们介绍过Class的静态方法Class.forName,理解类加载器有助于我们更好的理解该方法。

ClassLoader一般是系统提供的,不需要自己实现,不过,通过创建自定义的ClassLoader,可以实现一些强大灵活的功能,比如:

  • 热部署,在不重启Java程序的情况下,动态替换类的实现,比如Java Web开发中的JSP技术就利用自定义的ClassLoader实现修改JSP代码即生效,OSGI (Open Service Gateway Initiative)框架使用自定义ClassLoader实现动态更新。
  • 应用的模块化和相互隔离,不同的ClassLoader可以加载相同的类但互相隔离、互不影响。Web应用服务器如Tomcat利用这一点在一个程序中管理多个Web应用程序,每个Web应用使用自己的ClassLoader,这些Web应用互不干扰。OSGI利用这一点实现了一个动态模块化架构,每个模块有自己的ClassLoader,不同模块可以互不干扰。
  • 从不同地方灵活加载,系统默认的ClassLoader一般从本地的.class文件或jar文件中加载字节码文件,通过自定义的ClassLoader,我们可以从共享的Web服务器、数据库、缓存服务器等其他地方加载字节码文件。

理解自定义ClassLoader有助于我们理解这些系统程序和框架,如Tomat, JSP, OSGI,在业务需要的时候,也可以借助自定义ClassLoader实现动态灵活的功能。

下面,我们首先来进一步理解Java加载类的过程,理解类ClassLoader和Class.forName,介绍一个简单的应用,然后我们探讨如何实现自定义ClassLoader,演示如何利用它实现热部署。

类加载的基本机制和过程

运行Java程序,就是执行java这个命令,指定包含main方法的完整类名,以及一个classpath,即类路径。类路径可以有多个,对于直接的class文件,路径是class文件的根目录,对于jar包,路径是jar包的完整名称(包括路径和jar包名)。

Java运行时,会根据类的完全限定名寻找并加载类,寻找的方式基本就是在系统类和指定的类路径中寻找,如果是class文件的根目录,则直接查看是否有对应的子目录及文件,如果是jar文件,则首先在内存中解压文件,然后再查看是否有对应的类。

负责加载类的类就是类加载器,它的输入是完全限定的类名,输出是Class对象。类加载器不是只有一个,一般程序运行时,都会有三个:

  1. 启动类加载器(Bootstrap ClassLoader):这个加载器是Java虚拟机实现的一部分,不是Java语言实现的,一般是C++实现的,它负责加载Java的基础类,主要是<JAVA_HOME>/lib/rt.jar,我们日常用的Java类库比如String, ArrayList等都位于该包内。
  2. 扩展类加载器(Extension ClassLoader):这个加载器的实现类是sun.misc.Launcher$ExtClassLoader,它负责加载Java的一些扩展类,一般是<JAVA_HOME>/lib/ext目录中的jar包。
  3. 应用程序类加载器(Application ClassLoader):这个加载器的实现类是sun.misc.Launcher$AppClassLoader,它负责加载应用程序的类,包括自己写的和引入的第三方法类库,即所有在类路径中指定的类。

这三个类加载器有一定的关系,可以认为是父子关系,Application ClassLoader的父亲是Extension ClassLoader,Extension的父亲是Bootstrap ClassLoader,注意不是父子继承关系,而是父子委派关系,子ClassLoader有一个变量parent指向父ClassLoader,在子ClassLoader加载类时,一般会首先通过父ClassLoader加载,具体来说,在加载一个类时,基本过程是:

  1. 判断是否已经加载过了,加载过了,直接返回Class对象,一个类只会被一个ClassLoader加载一次。
  2. 如果没有被加载,先让父ClassLoader去加载,如果加载成功,返回得到的Class对象。
  3. 在父ClassLoader没有加载成功的前提下,自己尝试加载类。

这个过程一般被称为"双亲委派"模型,即优先让父ClassLoader去加载。为什么要先让父ClassLoader去加载呢?这样,可以避免Java类库被覆盖的问题,比如用户程序也定义了一个类java.lang.String,通过双亲委派,java.lang.String只会被Bootstrap ClassLoader加载,避免自定义的String覆盖Java类库的定义。

需要了解的是,"双亲委派"虽然是一般模型,但也有一些例外,比如:

  • 自定义的加载顺序:尽管不被建议,自定义的ClassLoader可以不遵从"双亲委派"这个约定,不过,即使不遵从,以"java"开头的类也不能被自定义类加载器加载,这是由Java的安全机制保证的,以避免混乱。
  • 网状加载顺序:在OSGI框架中,类加载器之间的关系是一个网,每个OSGI模块有一个类加载器,不同模块之间可能有依赖关系,在一个模块加载一个类时,可能是从自己模块加载,也可能是委派给其他模块的类加载器加载。
  • 父加载器委派给子加载器加载:典型的例子有JNDI服务(Java Naming and Directory Interface),它是Java企业级应用中的一项服务,具体我们就不介绍了。

一个程序运行时,会创建一个Application ClassLoader,在程序中用到ClassLoader的地方,如果没有指定,一般用的都是这个ClassLoader,所以,这个ClassLoader也被称为系统类加载器(System ClassLoader)。

下面,我们来具体看下表示类加载器的类 - ClassLoader。

理解ClassLoader

基本用法

类ClassLoader是一个抽象类,Application
ClassLoader和Extension
ClassLoader的具体实现类分别是sun.misc.Launcher$AppClassLoader和sun.misc.Launcher$ExtClassLoader,Bootstrap
ClassLoader不是由Java实现的,没有对应的类。

每个Class对象都有一个方法,可以获取实际加载它的ClassLoader,方法是:

public ClassLoader getClassLoader()

ClassLoader有一个方法,可以获取它的父ClassLoader:

public final ClassLoader getParent()

如果ClassLoader是Bootstrap ClassLoader,返回值为null。

比如:

public class ClassLoaderDemo {
public static void main(String[] args) {
ClassLoader cl = ClassLoaderDemo.class.getClassLoader();
while (cl != null) {
System.out.println(cl.getClass().getName());
cl = cl.getParent();
} System.out.println(String.class.getClassLoader());
}
}

输出为:

sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader
null

ClassLoader有一个静态方法,可以获取默认的系统类加载器:

public static ClassLoader getSystemClassLoader()

ClassLoader中有一个主要方法,用于加载类:

public Class<?> loadClass(String name) throws ClassNotFoundException

比如:

ClassLoader cl = ClassLoader.getSystemClassLoader();
try {
Class<?> cls = cl.loadClass("java.util.ArrayList");
ClassLoader actualLoader = cls.getClassLoader();
System.out.println(actualLoader);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

需要说明的是,由于委派机制,Class的getClassLoader()方法返回的不一定是调用loadClass的ClassLoader,比如,上面代码中,java.util.ArrayList实际由BootStrap ClassLoader加载,所以返回值就是null。

ClassLoader vs Class.forName

反射一节,我们介绍过Class的两个静态方法forName:

public static Class<?> forName(String className)
public static Class<?> forName(String name, boolean initialize, ClassLoader loader)

第一个方法使用系统类加载器加载,第二个指定ClassLoader,参数initialize表示,加载后,是否执行类的初始化代码(如static语句块),没有指定默认为true。

ClassLoader的loadClass方法与forName方法都可以加载类,它们有什么不同呢?基本是一样的,不过,有一个不同,ClassLoader的loadClass不会执行类的初始化代码,看个例子:

public class CLInitDemo {
public static class Hello {
static {
System.out.println("hello");
}
}; public static void main(String[] args) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
String className = CLInitDemo.class.getName() + "$Hello";
try {
Class<?> cls = cl.loadClass(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

使用ClassLoader加载静态内部类Hello,Hello有一个static语句块,输出"hello",运行该程序,类被加载了,但没有任何输出,即static语句块没有被执行。如果将loadClass的语句换为:

Class<?> cls = Class.forName(className);

则static语句块会被执行,屏幕将输出"hello"。

实现代码

我们来看下ClassLoader的loadClass代码,以进一步理解其行为:

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

它调用了另一个loadClass方法,其主要代码为(省略了一些代码,加了注释,以便于理解):

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先,检查类是否已经被加载了
Class c = findLoadedClass(name);
if (c == null) {
//没被加载,先委派父ClassLoader或BootStrap ClassLoader去加载
try {
if (parent != null) {
//委派父ClassLoader,resolve参数固定为false
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//没找到,捕获异常,以便尝试自己加载
}
if (c == null) {
// 自己去加载,findClass才是当前ClassLoader的真正加载方法
c = findClass(name);
}
}
if (resolve) {
// 链接,执行static语句块
resolveClass(c);
}
return c;
}
}

参数resolve类似Class.forName中的参数initialize,可以看出,其默认值为false,即使通过自定义ClassLoader重写loadClass,设置resolve为true,它调用父ClassLoader的时候,传递的也是固定的false。

findClass是一个protected方法,类ClassLoader的默认实现就是抛出ClassNotFoundException,子类应该重写该方法,实现自己的加载逻辑,后文我们会看个具体例子。

类加载应用 - 可配置的策略

可以通过ClassLoader的loadClass或Class.forName自己加载类,但什么情况需要自己加载类呢?

很多应用使用面向接口的编程,接口具体的实现类可能有很多,适用于不同的场合,具体使用哪个实现类在配置文件中配置,通过更改配置,不用改变代码,就可以改变程序的行为,在设计模式中,这是一种策略模式,我们看个简单的示例。

定义一个服务接口IService:

public interface IService {
public void action();
}

客户端通过该接口访问其方法,怎么获得IService实例呢?查看配置文件,根据配置的实现类,自己加载,使用反射创建实例对象,示例代码为:

public class ConfigurableStrategyDemo {
public static IService createService() {
try {
Properties prop = new Properties();
String fileName = "data/c87/config.properties";
prop.load(new FileInputStream(fileName));
String className = prop.getProperty("service");
Class<?> cls = Class.forName(className);
return (IService) cls.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
} public static void main(String[] args) {
IService service = createService();
service.action();
}
}

config.properties的内容示例为:

service=shuo.laoma.dynamic.c87.ServiceB

代码比较简单,就不赘述了。

自定义ClassLoader

基本用法

Java类加载机制的强大之处在于,我们可以创建自定义的ClassLoader,自定义ClassLoader是Tomcat实现应用隔离、支持JSP,OSGI实现动态模块化的基础。

怎么自定义呢?一般而言,继承类ClassLoader,重写findClass就可以了。怎么实现findClass呢?使用自己的逻辑寻找class文件字节码的字节形式,找到后,使用如下方法转换为Class对象:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)

name表示类名,b是存放字节码数据的字节数组,有效数据从off开始,长度为len。

看个例子:

public class MyClassLoader extends ClassLoader {

    private static final String BASE_DIR = "data/c87/";

    @Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = name.replaceAll("\\.", "/");
fileName = BASE_DIR + fileName + ".class";
try {
byte[] bytes = BinaryFileUtils.readFileToByteArray(fileName);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException ex) {
throw new ClassNotFoundException("failed to load class " + name, ex);
}
}
}

MyClassLoader从BASE_DIR下的路径中加载类,它使用了我们在57节介绍的BinaryFileUtils读取文件,转换为byte数组。MyClassLoader没有指定父ClassLoader,默认是系统类加载器,即ClassLoader.getSystemClassLoader()的返回值,不过,ClassLoader有一个可重写的构造方法,可以指定父ClassLoader:

protected ClassLoader(ClassLoader parent) 

用途

MyClassLoader有什么用呢?将BASE_DIR加到classpath中不就行了,确实可以,这里主要是演示基本用法,实际中,可以从Web服务器、数据库或缓存服务器获取bytes数组,这就不是系统类加载器能做到的了。

不过,不把BASE_DIR放到classpath中,而是使用MyClassLoader加载,确实有一个很大的好处,可以创建多个MyClassLoader,对同一个类,每个MyClassLoader都可以加载一次,得到同一个类的不同Class对象,比如:

MyClassLoader cl1 = new MyClassLoader();
String className = "shuo.laoma.dynamic.c87.HelloService";
Class<?> class1 = cl1.loadClass(className); MyClassLoader cl2 = new MyClassLoader();
Class<?> class2 = cl2.loadClass(className); if (class1 != class2) {
System.out.println("different classes");
}

cl1和cl2是两个不同的ClassLoader,class1和class2对应的类名一样,但它们是不同的对象。

这到底有什么用呢?

  • 可以实现隔离,一个复杂的程序,内部可能按模块组织,不同模块可能使用同一个类,但使用的是不同版本,如果使用同一个类加载器,它们是无法共存的,不同模块使用不同的类加载器就可以实现隔离,Tomcat使用它隔离不同的Web应用,OSGI使用它隔离不同模块。
  • 可以实现热部署,使用同一个ClassLoader,类只会被加载一次,加载后,即使class文件已经变了,再次加载,得到的也还是原来的Class对象,而使用MyClassLoader,则可以先创建一个新的ClassLoader,再用它加载Class,得到的Class对象就是新的,从而实现动态更新。

下面,我们来具体看热部署的示例。

自定义ClassLoader的应用 - 热部署

所谓热部署,就是在不重启应用的情况下,当类的定义,即字节码文件修改后,能够替换该Class创建的对象,怎么做到这一点呢?我们利用MyClassLoader,看个简单的示例。

我们使用面向接口的编程,定义一个接口IHelloService:

public interface IHelloService {
public void sayHello();
}

实现类是shuo.laoma.dynamic.c87.HelloImpl,class文件放到MyClassLoader的加载目录中。

演示类是HotDeployDemo,它定义了以下静态变量:

private static final String CLASS_NAME = "shuo.laoma.dynamic.c87.HelloImpl";
private static final String FILE_NAME = "data/c87/"
+CLASS_NAME.replaceAll("\\.", "/")+".class";
private static volatile IHelloService helloService;

CLASS_NAME表示实现类名称,FILE_NAME是具体的class文件路径,helloService是IHelloService实例。

当CLASS_NAME代表的类字节码改变后,我们希望重新创建helloService,反映最新的代码,怎么做呢?先看用户端获取IHelloService的方法:

public static IHelloService getHelloService() {
if (helloService != null) {
return helloService;
}
synchronized (HotDeployDemo.class) {
if (helloService == null) {
helloService = createHelloService();
}
return helloService;
}
}

这是一个单例模式,createHelloService()的代码为:

private static IHelloService createHelloService() {
try {
MyClassLoader cl = new MyClassLoader();
Class<?> cls = cl.loadClass(CLASS_NAME);
if (cls != null) {
return (IHelloService) cls.newInstance();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

它使用MyClassLoader加载类,并利用反射创建实例,它假定实现类有一个public无参构造方法。

在调用IHelloService的方法时,客户端总是先通过getHelloService获取实例对象,我们模拟一个客户端线程,它不停的获取IHelloService对象,并调用其方法,然后睡眠1秒钟,其代码为:

public static void client() {
Thread t = new Thread() {
@Override
public void run() {
try {
while (true) {
IHelloService helloService = getHelloService();
helloService.sayHello();
Thread.sleep(1000);
}
} catch (InterruptedException e) {
}
}
};
t.start();
}

怎么知道类的class文件发生了变化,并重新创建helloService对象呢?我们使用一个单独的线程模拟这一过程,代码为:

public static void monitor() {
Thread t = new Thread() {
private long lastModified = new File(FILE_NAME).lastModified(); @Override
public void run() {
try {
while (true) {
Thread.sleep(100);
long now = new File(FILE_NAME).lastModified();
if (now != lastModified) {
lastModified = now;
reloadHelloService();
}
}
} catch (InterruptedException e) {
}
}
};
t.start();
}

我们使用文件的最后修改时间来跟踪文件是否发生了变化,当文件修改后,调用reloadHelloService()来重新加载,其代码为:

public static void reloadHelloService() {
helloService = createHelloService();
}

就是利用MyClassLoader重新创建HelloService,创建后,赋值给helloService,这样,下次getHelloService()获取到的就是最新的了。

在主程序中启动client和monitor线程,代码为:

public static void main(String[] args) {
monitor();
client();
}

在运行过程中,替换HelloImpl.class,可以看到行为会变化,为便于演示,我们在data/c87/shuo/laoma/dynamic/c87/目录下准备了两个不同的实现类HelloImpl_origin.class和HelloImpl_revised.class,在运行过程中替换,会看到输出不一样,如下图所示:

使用cp命令修改HelloImpl.class,如果其内容与HelloImpl_origin.class一样,输出为"hello",如果与HelloImpl_revised.class一样,输出为"hello revised"。

完整的代码和数据在github上,文末有链接。

小结

本节探讨了Java中的类加载机制,包括Java加载类的基本过程,类ClassLoader的用法,以及如何创建自定义的ClassLoader,探讨了两个简单应用示例,一个通过动态加载实现可配置的策略,另一个通过自定义ClassLoader实现热部署。

84节到本节,我们探讨了Java中的多个动态特性,包括反射注解动态代理和类加载器,作为应用程序员,大部分用的都比较少,用的较多的就是使用框架和库提供的各种注解了,但这些特性大量应用于各种系统程序、框架、和库中,理解这些特性有助于我们更好的理解它们,也可以在需要的时候自己实现动态、通用、灵活的功能。

注解一节,我们提到,注解是一种声明式编程风格,它提高了Java语言的表达能力,日常编程中一种常见的需求是文本处理,在计算机科学中,有一种技术大大提高了文本处理的表达能力,那就是正则表达式,大部分编程语言都有对它的支持,它有什么强大功能呢?

(与其他章节一样,本节所有代码位于 https://github.com/swiftma/program-logic,位于包shuo.laoma.dynamic.c87下)

----------------

未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

Java编程的逻辑 (87) - 类加载机制的更多相关文章

  1. 《Java编程的逻辑》 - 文章列表

    <计算机程序的思维逻辑>系列文章已整理成书<Java编程的逻辑>,由机械工业出版社出版,2018年1月上市,各大网店有售,敬请关注! 京东自营链接:https://item.j ...

  2. Java编程的逻辑 (22) - 代码的组织机制

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  3. Java编程的逻辑 (67) - 线程的基本协作机制 (上)

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  4. Java编程的逻辑 (68) - 线程的基本协作机制 (下)

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  5. Java编程的逻辑 (86) - 动态代理

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  6. Java编程的逻辑 (84) - 反射

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  7. Java编程的逻辑 (13) - 类

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  8. Java编程的逻辑 (17) - 继承实现的基本原理

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  9. 《Java编程的逻辑》终于上市了!

    2018年1月下旬,<Java编程的逻辑>终于出版上市了! 这是老马过去两年死磕到底.无数心血的结晶啊! 感谢"博客园"的广大读者们,你们对老马文章的极高评价.溢美之词 ...

随机推荐

  1. BZOJ.1566.[NOI2009]管道取珠(DP 思路)

    BZOJ 洛谷 考虑\(a_i^2\)有什么意义:两个人分别操作原序列,使得得到的输出序列都为\(i\)的方案数.\(\sum a_i^2\)就是两人得到的输出序列相同的方案数. \(f[i][j][ ...

  2. 轻松掌握Redux-Action使用方法

    轻松掌握Redux-Action使用方法 Redux-Action主要有两个方法,createAction和createAction,只要掌握了这两个方法就会了redux-action的使用. cre ...

  3. BZOJ3945 : 无聊的邮递员

    因为两个人方案的对称性,可以将$k$除以$2$,转化为在$n-1$个间隔中设置若干断点,求第$k$小的增量. 对于选中的相邻的断点$(a,a+1)$和$(b,b+1)$,增量为$|x_a-x_{b+1 ...

  4. NIOH

    目录 NIOH中的双刀与阴阳术的应用 作战准备篇 2周目毕业装备: 加点: 双刀: 核心技能: 还行的技能: 被动技能: 忍术: 阴阳术: 必学: 选学: 守护灵: 隐世茶室 & 铁匠铺 出发 ...

  5. [P1005][NOIP2007] 矩阵取数游戏 (DP+高精)

    我不会高精…… 也不会DP…… 这道题即考高精又考DP…… 我要死了 给一个不是高精的代码(当然不能满分) #include<cstdio> #include<iostream> ...

  6. 暗之的锁链 [COGS2434] [树上差分]

    Description 无向图中有N个节点和两类边,一类边被称为主要边,而另一类被称为附加边.Dark有N – 1条主要边,并且Dark的任意两个节点之间都存在一条只由主要边构成的路径.另外,Dark ...

  7. IIS 日志分析工具:Log Parser Studio

    1.安装Log Parser,下载地址:http://www.microsoft.com/en-us/download/details.aspx?displaylang=en&id=24659 ...

  8. 前端里面如何进行搜索引擎优化(SEO)

    如何进行SEO优化: (1) 避免head标签js堵塞: 所有放在head标签里面的js和css都会堵塞渲染:如果这些css和js需要加载很久的话,那么页面就空白了: 解决办法:一是把script放到 ...

  9. C语言中字符输入问题

    先上例题,一道太水太水的题, http://acm.hdu.edu.cn/showproblem.php?pid=1170 让做一个简单的计算器.然而入坑了. #include<stdio.h& ...

  10. 大数据hadoop生态圈

    大数据本身是个很宽泛的概念,Hadoop生态圈(或者泛生态圈)基本上都是为了处理超过单机尺度的数据处理而诞生的.你可以把它比作一个厨房所以需要的各种工具.锅碗瓢盆,各有各的用处,互相之间又有重合.你可 ...