Java重复类问题探究

集成开发环境(IDE)是一把双刃剑,为我们提供莫大便利的同时也隐藏了很多的问题。一旦出现问题,如果不了解内部的一些机制会让我们手足无措。本文抛开IDE,用最原始的方式还原重复类引发的一个问题,希望能给大家一点启发。

重复类的定义

重复类是指程序中存在两个或者多个包名以及类名都一致的类。如果只是类名一致,而包名不一致,这本身就是不同的类,不在本文的讨论之列。而包名和类名完全一致但是加载它们的类加载器(ClassLoader)不一样,则其实在JVM的层面,它们也属于不同的类,也不在我们本文讨论的重复类范围。

本文要讨论的重复类是指类名和包名完全一样,并且不会被不同的类加载器分别加载的情况,也就是说这两个类只会有一个被加载到虚拟机中。

哪种情况下会引入重复类呢?
比如maven中引入了两个不同jar包依赖,而这两个不同的jar包均包含同一个类。
需要指出的是,如果这两个类包括内容都是完全一样的,那不会引起什么问题。因此本文讨论的重复类,是指类名一致,但是内容不一致的情况。

重复类可能引起的问题

如果说两个重复类中的内容不一致,则很有可能在系统运行的时候会报错,比如报找不到某个方法的错(java.lang.NoSuchMethodError)。
当然除了NoSuchMethodError之外还可能出现其他的各种异常,需要具体问题具体分析。

重复类举例

假设我们当前的目录结构如下所示:

abinge@abinge-ubuntu:~/workspace$ pwd
/home/abinge/workspace
abinge@abinge-ubuntu:~/workspace$ tree
.
├── classes
│   └── cn
│       └── codecrazy
│           └── App.class
├── lib
│   ├── common-1.jar
│   └── common-2.jar
└── src
    ├── cn
    │   └── codecrazy
    │       └── App.java
    └── my
        └── Hello.java
  • lib目录用来存放外部依赖的类(jar包)
  • src目录中存放我们的应用源文件
  • classes目录中存放我们的应用类编译之后的class文件

App.java的内容如下所示:

package cn.codecrazy;
import my.Hello;

public class App {
  public static void main(String[] args) {
    Hello hello = new Hello();
    System.out.println(hello.sayHello());
  }
}

App.java是我们的主类,该类中引用了my.Hello,而my.Hello的class文件已经打包到lib目录下的common-1.jarcommon-2.jar中了。这两个jar包中都包含了my.Hello.class文件,但是类的内容不一样。common-1.jar中的class文件对应的java源文件为src/my/Hello.java,内容如下:

package my;
public class Hello {
  public String sayHello() {
    return "Hello";
  }
}

该类只有一个方法,sayHello(),而common-2.jar中存在同样的my.Hello.class文件,只不过该class文件对应的源文件如下所示:

package my;
public class Hello {
  
}

即去掉了sayHello()方法
因此common-1.jarcommon-2.jar中包含了相同的类,但是类的内容不一样。

之后我们用javac命令编译App.java:

javac -d classes src/cn/codecrazy/App.java -classpath lib/common-1.jar:lib/common-2.jar

javac命令的-d选项后面跟着 classes 表示将最终生成的class文件输出到classes目录下面,-classpath后面指定我们javac编译文件时搜索的类路径。如果有多个路径的话中间用冒号分隔(Windows下面是用分号分隔)。

由于我们的App.java中用到了my.Hello,因此需要指定classpath,否则编译的时候会找不到my.Hello这个符号,从而编译失败。

编译完之后会在classes/cn/codecrazy下面生成一个App.class,多出来的cn/codecrazy目录是自动生成的,因为App.java中的包名是cn.codecrazy

编译成功之后,我们再来运行App.class,在workspace目录下面,执行如下命令:

java -classpath classes:lib/common-2.jar:lib/common-1.jar cn.codecrazy.App 

执行过程会报如下错误:

Exception in thread "main" java.lang.NoSuchMethodError: my.Hello.sayHello()Ljava/lang/String;
    at cn.codecrazy.App.main(App.java:7)

重复类报错原因分析

报错原因很明显,是因为运行的时候加载了那个没有sayHello方法的类导致运行时报错,那为什么会去加载那个没有sayHello方法的类呢?这是因为我们执行java命令的时候-classpath 那里将common-2.jar放在了common-1.jar前面,类加载器加载类的时候一旦在前面已经加载到它要加载的my.Hello.class的时候就不会再去加载其他的my.Hello.class类,因此到了执行的时候就会发现加载的那个my.Hello.class类没有sayHello方法,从而报错。

可能有人会说,javac编译的时候common-1.jar就是放在前面的,为什么在java命令这里故意把common-2.jar放前面从而引起报错呢?对于这个问题,大家不要忘了,实际场景中,编译和运行本来就是两个不同的过程,甚至很多是在本地编译,然后放到远程去运行的,这种时候你怎么保证远程执行的时候就会按照你编译时指定的classpath的顺序去运行java程序。

对于我们这个例子,如果在编译的时候就把common-2.jar放在前面,那编译就直接通不过了,因为编译器会发现App.java中调用了my.Hello类中的syaHello方法,但是编译器找到的那个my.Hello类中没有那个方法,因此编译直接报错。

我这个例子中的错误正好是个编译时就能发现的错误,很多错误可能是运行时才能发现的,因此那种情况下,就算是编译和运行时指定的classpath路径顺序一致的话还是会存在编译不报错,运行报错的情况。

从本质上来说,之所以会出现这些问题,就是因为classpath下面存在了相同名称但是内容不一致的类,而正好又是那个有问题的类被加载进JVM中运行。

希望本文能给大家带来一点启发。篇幅所限,有许多相关的主题没有在本文进行更发散和深入的分析,留待以后再进行分享。

您的关注是我不断创作的动力源泉!期待认识更多的朋友,一起交流Java相关技术栈,共同进步!阅读更多技术文章,可关注我的公众号:codecrazy4j

Java重复类问题探究_第1张图片
阿斌哥的博客

你可能感兴趣的:(Java重复类问题探究)