はじめに
Javaの便利なアルゴリズムを実装したは良いものの、提出するときにファイルがバラバラで時間がかかる。。。そんなお悩みありませんか?
この記事では、記述したプログラムを競技プログラミングで提出するために、ライブラリのクラスを含めて1つのファイルにまとめるスクリプトおよび環境を構築します。さあ、Javaで快適競プロライフをスタートしましょう!!
この記事で達成できること
- インポートしたライブラリのクラスおよびエントリポイントを、1クリックで1つのファイルにまとめられる
- 1クリックでコードのテストができる
前提条件・対象読者
- 言語経験:Java
- 開発環境:IntelliJ IDEA / VS Codeなど + Java 17 + Gradleプロジェクト
- 競プロ入門者・経験者
IDEはGradleプロジェクトのタスクが実行できるのであれば何でも良いです。
アプローチの概要
全体の流れとしては、
- Main.javaを起点として、import文に含まれるクラスを認識する
- そのクラスが標準ライブラリならば、import文を保持する
- そのクラスが自作ライブラリならば、それを含むjavaファイルを読みに行く
- そのjavaファイルを起点として、1.に再帰
クラスファイルの読み取りには、JavaParserというライブラリを利用します。公式のGitリポジトリはこちらです。
一応明記しておきますが、クラスの動的ロードには対応する予定はありません。競プロでそれをする必要が発生するのかわかりませんが。
Gradleプロジェクトの作成
本記事では割愛します。IntelliJ IDEAのユーザーの方は、こちらの公式サイトを参考にしていただければよいと思います。
バンドルスクリプトの記述
ソースフォルダ内のどこかに、以下の内容に相当するクラスファイルを作成します。自作ライブラリかどうかを、FQCNがcom.guy7cc.abclib4jで始まるかどうかで判定していますが、皆さんは別のパッケージにライブラリを作られると思うので、適宜変更してください。
Bundler.java
package com.guy7cc.abclib4j.tools;
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.expr.FieldAccessExpr;
import com.github.javaparser.ast.expr.MethodCallExpr;
import com.github.javaparser.ast.expr.NameExpr;
import com.github.javaparser.ast.nodeTypes.NodeWithName;
import com.github.javaparser.printer.PrettyPrinter;
import com.github.javaparser.printer.configuration.Indentation;
import com.github.javaparser.printer.configuration.PrettyPrinterConfiguration;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
public class Bundler {
public static void main(String[] args) throws IOException {
StaticJavaParser.getConfiguration().setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_17);
Path sourceRoot = Paths.get("src/main/java");
Path mainJavaPath = sourceRoot.resolve("com/guy7cc/abclib4j/Main.java");
Path outputPretty = Paths.get("build/generated/Main.java");
Path outputMinify = Paths.get("build/generated/Main.minify.java");
Files.createDirectories(outputPretty.getParent());
Queue<Path> localLibPaths = new ArrayDeque<>();
localLibPaths.add(mainJavaPath);
Set<Path> resultLocalLibPaths = new HashSet<>(Set.of(mainJavaPath));
while(!localLibPaths.isEmpty()){
Path path = localLibPaths.poll();
CompilationUnit cu = StaticJavaParser.parse(path);
Set<Path> newLocalLibs = collectLocalLibraries(sourceRoot, cu);
for(Path newLocalLib : newLocalLibs){
if(!resultLocalLibPaths.contains(newLocalLib)){
localLibPaths.add(newLocalLib);
resultLocalLibPaths.add(newLocalLib);
}
}
}
SortedSet<CompilationUnit> localLibCUs = new TreeSet<>(Comparator.comparing(cu -> cu.getPrimaryTypeName().get()));
SortedSet<TypeDeclaration<?>> localLibs = new TreeSet<>(Comparator.comparing(td -> td.getFullyQualifiedName().get()));
for(Path localLib : resultLocalLibPaths){
CompilationUnit cu = StaticJavaParser.parse(localLib);
resolveStaticImports(sourceRoot, cu);
localLibCUs.add(cu);
for(TypeDeclaration<?> type : cu.getTypes()){
if(!type.getNameAsString().equals("Main"))
type.removeModifier(Modifier.Keyword.PUBLIC);
localLibs.add(type);
}
}
Set<ImportDeclaration> importLibs = new HashSet<>();
for(CompilationUnit cu : localLibCUs){
for(ImportDeclaration id : cu.getImports()){
if(!id.getNameAsString().startsWith("com.guy7cc.abclib4j")){
importLibs.add(id);
}
}
}
CompilationUnit resultCU = new CompilationUnit();
resultCU.removePackageDeclaration();
importLibs.forEach(resultCU::addImport);
localLibs.forEach(resultCU::addType);
PrettyPrinterConfiguration pretty = new PrettyPrinterConfiguration();
pretty.setIndentation(new Indentation(Indentation.IndentType.SPACES, 4));
pretty.setOrderImports(true);
pretty.setPrintComments(true);
pretty.setPrintJavadoc(true);
pretty.setEndOfLineCharacter(System.lineSeparator());
PrettyPrinter prettyPrinter = new PrettyPrinter(pretty);
PrettyPrinterConfiguration minify = new PrettyPrinterConfiguration();
minify.setIndentation(new Indentation(Indentation.IndentType.SPACES, 1));
minify.setOrderImports(true);
minify.setPrintComments(false);
minify.setPrintJavadoc(false);
minify.setEndOfLineCharacter("");
PrettyPrinter minifyPrinter = new PrettyPrinter(minify);
Files.writeString(outputPretty, prettyPrinter.print(resultCU));
System.out.println("Main.java generated at: " + outputPretty);
Files.writeString(outputMinify, minifyPrinter.print(resultCU));
System.out.println("Main.minify.java generated at: " + outputMinify);
}
private static Set<Path> collectLocalLibraries(Path sourceRoot, CompilationUnit cu){
Set<Path> localLibraries = new HashSet<>();
Set<String> imported = cu.getImports().stream()
.filter(id -> !id.isStatic() && !id.isAsterisk())
.map(ImportDeclaration::getNameAsString)
.collect(Collectors.toSet());
for (String fqcn : imported) {
if (fqcn.startsWith("com.guy7cc.abclib4j")) {
Path relPath = Paths.get(fqcn.replace('.', '/') + ".java");
Path fullPath = sourceRoot.resolve(relPath);
if (Files.exists(fullPath)) {
localLibraries.add(fullPath);
}
}
}
Set<String> astersik = cu.getImports().stream()
.filter(id -> !id.isStatic() && id.isAsterisk())
.map(ImportDeclaration::getNameAsString)
.collect(Collectors.toSet());
for(String fqcn : astersik){
if (fqcn.startsWith("com.guy7cc.abclib4j")) {
Path dirRelPath = Paths.get(fqcn.replace('.', '/'));
Path dirFullPath = sourceRoot.resolve(dirRelPath);
if(Files.exists(dirFullPath)){
for(File file : dirFullPath.toFile().listFiles()){
Path fullPath = file.toPath();
localLibraries.add(fullPath);
}
}
}
}
Set<String> statik = cu.getImports().stream()
.filter(id -> id.isStatic() && !id.isAsterisk())
.map(ImportDeclaration::getNameAsString)
.collect(Collectors.toSet());
for (String fqcn : statik) {
if (fqcn.startsWith("com.guy7cc.abclib4j")) {
Path relPath = Paths.get(fqcn.substring(0, fqcn.lastIndexOf('.')).replace('.', '/') + ".java");
Path fullPath = sourceRoot.resolve(relPath);
if (Files.exists(fullPath)) {
localLibraries.add(fullPath);
}
}
}
Set<String> statikAsterisk = cu.getImports().stream()
.filter(id -> id.isStatic() && id.isAsterisk())
.map(ImportDeclaration::getNameAsString)
.collect(Collectors.toSet());
for (String fqcn : statikAsterisk) {
if (fqcn.startsWith("com.guy7cc.abclib4j")) {
Path relPath = Paths.get(fqcn.replace('.', '/') + ".java");
Path fullPath = sourceRoot.resolve(relPath);
if (Files.exists(fullPath)) {
localLibraries.add(fullPath);
}
}
}
return localLibraries;
}
public static void resolveStaticImports(Path sourceRoot, CompilationUnit cu) {
Map<String, String> staticIdentifiers = new HashMap<>();
List<ImportDeclaration> staticImports = cu.getImports().stream()
.filter(ImportDeclaration::isStatic)
.toList();
for (ImportDeclaration id : staticImports) {
String importName = id.getNameAsString();
if (!id.isAsterisk()) {
// import static xxx.yyy.ZZZ.method;
String[] parts = importName.split("\\.");
if(importName.startsWith("com.guy7cc.abclib4j")){
String className = parts[parts.length - 2];
String memberName = parts[parts.length - 1];
staticIdentifiers.put(memberName, className);
} else {
String memberName = parts[parts.length - 1];
staticIdentifiers.put(memberName, importName.substring(0, importName.lastIndexOf('.')));
}
} else {
// import static xxx.yyy.ZZZ.*;
String classFqcn = importName;
String className = classFqcn.substring(classFqcn.lastIndexOf('.') + 1);
if(classFqcn.startsWith("com.guy7cc.abclib4j")){
Path classPath = sourceRoot.resolve(classFqcn.replace('.', '/') + ".java");
if (Files.exists(classPath)) {
try {
CompilationUnit importedCU = StaticJavaParser.parse(classPath);
// static method
importedCU.findAll(MethodDeclaration.class).forEach(md -> {
if (md.isPublic() && md.isStatic()) {
staticIdentifiers.put(md.getNameAsString(), className);
}
});
// static field
importedCU.findAll(FieldDeclaration.class).forEach(fd -> {
if (fd.isPublic() && fd.isStatic()) {
fd.getVariables().forEach(var -> staticIdentifiers.put(var.getNameAsString(), className));
}
});
} catch (IOException e) {
System.err.println("Failed to parse: " + classPath + ": " + e.getMessage());
}
} else {
System.err.println("Class not found for static import: " + classPath);
}
} else {
try {
Class<?> clazz = Class.forName(classFqcn);
for (Method m : clazz.getDeclaredMethods()) {
if (java.lang.reflect.Modifier.isPublic(m.getModifiers()) && java.lang.reflect.Modifier.isStatic(m.getModifiers())) {
staticIdentifiers.put(m.getName(), clazz.getCanonicalName());
}
}
for (Field f : clazz.getDeclaredFields()) {
if (java.lang.reflect.Modifier.isPublic(f.getModifiers()) && java.lang.reflect.Modifier.isStatic(f.getModifiers())) {
staticIdentifiers.put(f.getName(), clazz.getCanonicalName());
}
}
} catch (ClassNotFoundException e) {
System.err.println("Failed to load standard class: " + classFqcn);
}
}
}
}
// Replace method call: max() → Math.max()
cu.findAll(MethodCallExpr.class).forEach(mce -> {
if (mce.getScope().isEmpty()) {
String name = mce.getNameAsString();
if (staticIdentifiers.containsKey(name)) {
mce.setScope(new NameExpr(staticIdentifiers.get(name)));
}
}
});
// Replace field access: PI → Math.PI
cu.findAll(FieldAccessExpr.class).forEach(ne -> {
String name = ne.getNameAsString();
if (staticIdentifiers.containsKey(name)) {
Optional<Node> parent = ne.getParentNode();
if (parent.isEmpty() || !(parent.get() instanceof MethodCallExpr)) {
FieldAccessExpr fieldAccess = new FieldAccessExpr(
new NameExpr(staticIdentifiers.get(name)), name
);
ne.replace(fieldAccess);
}
}
});
// remove import static
cu.getImports().removeIf(ImportDeclaration::isStatic);
}
}
build.gradle.ktsの記述
ビルドスクリプトは、以下のように記述します。
build.gradle.kts
plugins {
java
}
repositories {
mavenCentral()
}
dependencies {
implementation("com.github.javaparser:javaparser-core:3.26.4")
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
tasks.register<JavaExec>("generateSingleJava") {
group = "submission"
description = "Generate Main.java for submission"
classpath = sourceSets["main"].runtimeClasspath
mainClass.set("com.guy7cc.abclib4j.tools.Bundler")
dependsOn("classes")
}
これを記述した後にGradleプロジェクトをリロードすると、submissionタスクグループにgenerateSingleJavaというタスクが作成されていると思います。こちらを実行すると、
(ルートディレクトリ)/app/build/generated/
このディレクトリにMain.javaとMain.minify.javaが作成されます。このファイルの中身のどちらかを提出することができます。
ちなみに、Main.javaは比較的可読性の高いコードで、Main.minify.javaはインデントや改行などを削減して、提出ファイルサイズの上限をくぐって出す用のコードです。
コードテスト方法
IntelliJ IDEAを使用しているならば、単純にMain.javaをエントリポイントとして実行する構成を作成してください。これを実行もしくはデバッグ開始し、下のコンソールに入力してテストすることができます。
まとめ
多くの競技プログラミングコンテストでは、いかに速く正確に問題を解くかが問われます。今回整えた環境によって、Javaにとって最適なパッケージ切りを維持したまま、問題だけに集中して臨むことができます。よい競技プログラミングライフを!
既知の不具合
- import static文によってインポートされたクラスの静的フィールドに対し、バンドル実行後にクラス名が付加されない。
コメント