集成开发环境(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.jar
和common-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.jar
和common-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