Java 编译器(javac)在解析类时的优先级策略

5 阅读5分钟

关于 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 = truehaveSourcePath = 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.

原因包括:

  1. 开发一致性:你在修改 BTest.java,希望编译器使用最新代码,而不是旧的 JAR。
  2. 增量编译:避免因 JAR 版本滞后导致错误。
  3. 调试友好:确保编译结果与当前源码一致。

四、如何强制使用 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
        }
    }

}