关于 Java 编译器(javac)在解析类时的优先级策略:当同时存在源文件(.java)和已编译的类文件(.class,比如来自 JAR 包)时,javac 会优先使用源文件进行编译,而不是使用 JAR 中的 .class 文件。
下面结合你提供的 javac 源码片段和命令行参数,详细解释为什么会出现“先使用 src 下的 BTest.java 而不是 lib 下 B.jar 包中的 BTest.class”的现象。
一、你的 javac 命令
bash编辑
javac -sourcepath D:...\src\main\java D:...\NumberFormatTest.java
-sourcepath指定了 源代码搜索路径。- 编译器在解析
new BTest()时,需要找到BTest类的定义。 - 此时,如果
BTest.java存在于-sourcepath指定的目录中(即src/main/java/com/newland/mi/o2o/BTest.java),javac 会优先从源码编译它,而不是去 classpath(或 module path)中找已编译的.class。
⚠️ 注意:
-sourcepath的存在会触发“源码优先”行为。即使你没有显式指定-classpath,只要-sourcepath包含了某个类的.java文件,javac 就会尝试编译它。
二、从你提供的 javac 源码看类加载逻辑
1. loadClass() 方法(入口)
java编辑
public ClassSymbol loadClass(ModuleSymbol msym, Name flatname) throws CompletionFailure { ... ClassSymbol c = syms.enterClass(ps.modle, flatname); if (c.members_field == null) { c.complete(); // ← 触发 complete() } return c; }
- 当 javac 遇到
new BTest(),会调用loadClass(..., "com.newland.mi.o2o.BTest")。 - 如果符号表中还没有
BTest的完整信息(members_field == null),就调用complete()。
2. complete() 方法(分发到 fillIn)
java编辑
private void complete(Symbol sym) { if (sym.kind == TYP) { ClassSymbol c = (ClassSymbol) sym; fillIn(c); // ← 实际加载逻辑 } }
3. fillIn(ClassSymbol c) 的关键逻辑
- 它会检查
c.classfile(即该类对应的.class文件对象)。 - 但
c.classfile是怎么来的? → 来自 文件查找阶段(scanUserPaths) 。
4. scanUserPaths():决定从哪里加载
这是关键!在包符号(PackageSymbol p)初始化时,会扫描路径:
java编辑
private void scanUserPaths(PackageSymbol p, boolean includeSourcePath) { ... if (wantSourceFiles && !haveSourcePath) { fillIn(p, CLASS_PATH, list(CLASS_PATH, ...)); } else { if (wantClassFiles) fillIn(p, CLASS_PATH, list(CLASS_PATH, ..., classKinds)); if (wantSourceFiles) fillIn(p, SOURCE_PATH, list(SOURCE_PATH, ..., sourceKinds)); // ← 先查 SOURCE_PATH } }
- 因为你指定了
-sourcepath,所以includeSourcePath = true,haveSourcePath = true。 - 于是,先扫描
SOURCE_PATH找.java文件,再扫描CLASS_PATH找.class。 - 一旦在
SOURCE_PATH中找到BTest.java,就会创建一个ClassSymbol并关联其源文件(.java)作为classfile(虽然名字叫 classfile,但对源码来说其实是 JavaFileObject of kind SOURCE) 。
🔍 在
DirectoryFileObject构造中,你看到它接受Path和RelativePath,说明它能代表源文件或类文件。
5. 回到 fillIn():
java编辑
if (classfile.getKind() == JavaFileObject.Kind.CLASS) { reader.readClassFile(c); } else { sourceCompleter.complete(c); // ← 编译 .java 源码! }
- 因为
BTest.java来自-sourcepath,所以classfile.getKind() == SOURCE。 - 于是走
else分支,调用sourceCompleter.complete(c),现场编译BTest.java。 - 根本不会去碰 JAR 包里的
BTest.class!
三、为什么这样设计?
这是 javac 的标准行为,符合 Java 语言规范的精神:
If a type is found both as a source file and as a compiled class file, the source file takes precedence during compilation.
原因包括:
- 开发一致性:你在修改
BTest.java,希望编译器使用最新代码,而不是旧的 JAR。 - 增量编译:避免因 JAR 版本滞后导致错误。
- 调试友好:确保编译结果与当前源码一致。
四、如何强制使用 JAR 中的 BTest.class?
如果你不想编译 src 下的 BTest.java,有几种方法:
✅ 方法 1:不要把 BTest.java 放在 -sourcepath 路径下
- 移除
BTest.java,或将其移到非 sourcepath 目录。
✅ 方法 2:不使用 -sourcepath,改用 -classpath
bash编辑
javac -cp lib/B.jar D:...\NumberFormatTest.java
- 这样 javac 不会主动搜索源码,只从 classpath 加载
.class。
✅ 方法 3:显式排除 BTest.java(不推荐)
- 无法直接排除,但可通过构建工具(如 Maven/Gradle)控制 source roots。
总结
表格
| 原因 | 说明 |
|---|---|
| -sourcepath 指定了源码路径 | javac 会优先在此路径下查找 .java 文件 |
| scanUserPaths() 先查 SOURCE_PATH | 找到 BTest.java 后,将其作为类的来源 |
| fillIn() 调用 sourceCompleter | 现场编译源码,忽略 JAR 中的 .class |
| 这是 javac 的预期行为 | 源码优先于字节码,保证开发一致性 |
因此,只要 BTest.java 出现在 -sourcepath 指定的目录中,javac 就一定会编译它,而不会使用 JAR 包中的版本。
##源码
void fillIn(ClassSymbol c) {
if (completionFailureName == c.fullname) {
throw new CompletionFailure(
c, () -> diagFactory.fragment(Fragments.UserSelectedCompletionFailure), dcfh);
}
currentOwner = c;
JavaFileObject classfile = c.classfile;
if (classfile != null) {
JavaFileObject previousClassFile = currentClassFile;
Symbol prevOwner = c.owner;
Name prevName = c.fullname;
try {
if (reader.filling) {
Assert.error("Filling " + classfile.toUri() + " during " + previousClassFile);
}
currentClassFile = classfile;
if (verbose) {
log.printVerbose("loading", currentClassFile.getName());
}
if (classfile.getKind() == JavaFileObject.Kind.CLASS) {
reader.readClassFile(c);
c.flags_field |= getSupplementaryFlags(c);
} else {
if (!sourceCompleter.isTerminal()) {
sourceCompleter.complete(c);
} else {
throw new IllegalStateException("Source completer required to read "
+ classfile.toUri());
}
}
} catch (BadClassFile cf) {
//the symbol may be partially initialized, purge it:
c.owner = prevOwner;
c.members_field.getSymbols(sym -> sym.kind == TYP).forEach(sym -> {
ClassSymbol csym = (ClassSymbol) sym;
csym.owner = sym.packge();
csym.owner.members().enter(sym);
csym.fullname = sym.flatName();
csym.name = Convert.shortName(sym.flatName());
csym.reset();
});
c.fullname = prevName;
c.name = Convert.shortName(prevName);
c.reset();
throw cf;
} finally {
currentClassFile = previousClassFile;
}
} else {
throw classFileNotFound(c);
}
}
private void complete(Symbol sym) throws CompletionFailure {
if (sym.kind == TYP) {
try {
ClassSymbol c = (ClassSymbol) sym;
dependencies.push(c, CompletionCause.CLASS_READER);
annotate.blockAnnotations();
c.members_field = new Scope.ErrorScope(c); // make sure it's always defined
completeOwners(c.owner);
completeEnclosing(c);
fillIn(c);
} finally {
annotate.unblockAnnotationsNoFlush();
dependencies.pop();
}
} else if (sym.kind == PCK) {
PackageSymbol p = (PackageSymbol)sym;
try {
fillIn(p);
} catch (IOException ex) {
throw new CompletionFailure(
sym,
() -> diagFactory.fragment(
Fragments.ExceptionMessage(ex.getLocalizedMessage())),
dcfh)
.initCause(ex);
}
}
if (!reader.filling)
annotate.flush(); // finish attaching annotations
}
public ClassSymbol loadClass(ModuleSymbol msym, Name flatname) throws CompletionFailure {
Assert.checkNonNull(msym);
Name packageName = Convert.packagePart(flatname);
PackageSymbol ps = syms.lookupPackage(msym, packageName);
Assert.checkNonNull(ps.modle, () -> "msym=" + msym + "; flatName=" + flatname);
boolean absent = syms.getClass(ps.modle, flatname) == null;
ClassSymbol c = syms.enterClass(ps.modle, flatname);
if (c.members_field == null) {
try {
c.complete();
} catch (CompletionFailure ex) {
if (absent) {
syms.removeClass(ps.modle, flatname);
ex.dcfh.classSymbolRemoved(c);
}
throw ex;
}
}
return c;
}
private DirectoryFileObject(BaseFileManager fileManager, Path path,
Path userPackageRootDir, RelativePath relativePath) {
super(fileManager, path);
this.userPackageRootDir = Objects.requireNonNull(userPackageRootDir);
this.relativePath = relativePath;
}
private void scanUserPaths(PackageSymbol p, boolean includeSourcePath) throws IOException {
Set<JavaFileObject.Kind> kinds = getPackageFileKinds();
Set<JavaFileObject.Kind> classKinds = EnumSet.copyOf(kinds);
classKinds.remove(JavaFileObject.Kind.SOURCE);
boolean wantClassFiles = !classKinds.isEmpty();
Set<JavaFileObject.Kind> sourceKinds = EnumSet.copyOf(kinds);
sourceKinds.remove(JavaFileObject.Kind.CLASS);
boolean wantSourceFiles = !sourceKinds.isEmpty();
boolean haveSourcePath = includeSourcePath && fileManager.hasLocation(SOURCE_PATH);
if (verbose && verbosePath) {
verbosePath = false; // print once per compile
if (fileManager instanceof StandardJavaFileManager standardJavaFileManager) {
if (haveSourcePath && wantSourceFiles) {
List<Path> path = List.nil();
for (Path sourcePath : standardJavaFileManager.getLocationAsPaths(SOURCE_PATH)) {
path = path.prepend(sourcePath);
}
log.printVerbose("sourcepath", path.reverse().toString());
} else if (wantSourceFiles) {
List<Path> path = List.nil();
for (Path classPath : standardJavaFileManager.getLocationAsPaths(CLASS_PATH)) {
path = path.prepend(classPath);
}
log.printVerbose("sourcepath", path.reverse().toString());
}
if (wantClassFiles) {
List<Path> path = List.nil();
for (Path platformPath : standardJavaFileManager.getLocationAsPaths(PLATFORM_CLASS_PATH)) {
path = path.prepend(platformPath);
}
for (Path classPath : standardJavaFileManager.getLocationAsPaths(CLASS_PATH)) {
path = path.prepend(classPath);
}
log.printVerbose("classpath", path.reverse().toString());
}
}
}
String packageName = p.fullname.toString();
if (wantSourceFiles && !haveSourcePath) {
fillIn(p, CLASS_PATH,
list(CLASS_PATH,
p,
packageName,
kinds));
} else {
if (wantClassFiles)
fillIn(p, CLASS_PATH,
list(CLASS_PATH,
p,
packageName,
classKinds));
if (wantSourceFiles)
fillIn(p, SOURCE_PATH,
list(SOURCE_PATH,
p,
packageName,
sourceKinds));
}
}
javac
-sourcepath D:\IdeaProjects\idea_mi_java\miJava\mi-bug-fix\src\main\java D:\IdeaProjects\idea_mi_java\miJava\mi-bug-fix\src\main\java\com\newland\mi\o2o\NumberFormatTest.java
public class NumberFormatTest {
public static void main(String[] args) {
try {
int f = (int)Float.parseFloat("2.0");
System.out.println(f);
System.out.println(new BTest());
} catch (NumberFormatException e) {
e.printStackTrace();
// 可选:设默认值,如 0 或 null
}
}
}