Error [java.lang.NoSuchMethodError: java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer;]

NoSuchMethodError: java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer; の直し方

FIX SUMMARY verified
Applies when
eclipse-temurin:17-jdkbuilt with -source/-target 8 on JDK 17 (which does not restrict the API), then run on a Java 8 JVM; Buffer methods became covariant in Java 9

Verified: reproduced in eclipse-temurin:17-jdk, then the java.lang.NoSuchMethodError: java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer; signature was gone after the fix (exit 0).

新しい JDK でビルドしたアプリを古い JVM で動かすと、次のエラーで落ちることがあります。

Exception in thread "main" java.lang.NoSuchMethodError: java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer;
	at App.main(App.java:7)

コピペ検索用の表記ゆれです。メソッド名が違うだけで、原因はどれも同じです。

  • NoSuchMethodError: java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer;
  • NoSuchMethodError: java.nio.ByteBuffer.clear()Ljava/nio/ByteBuffer;
  • NoSuchMethodError: java.nio.ByteBuffer.position(I)Ljava/nio/ByteBuffer;
  • NoSuchMethodError: java.nio.ByteBuffer.rewind()Ljava/nio/ByteBuffer;
  • NoSuchMethodError: java.nio.CharBuffer.flip()Ljava/nio/CharBuffer;

flip() は Java 8 にもあります。 メソッドは実在するのに、実行時に見つかりません。

原因はビルドの指定です。-source 8 -target 8 でビルドしていると、クラスファイルは Java 8 と名乗るのに、中身は新しい JDK の API を呼びます

直し方

-source / -target をやめて、--release にします。それだけです。

# Before(このエラーになる)
javac -source 8 -target 8 -d out App.java

# After
javac --release 8 -d out App.java

Maven:

<properties>
  <!-- maven.compiler.source / maven.compiler.target ではなく release を使う -->
  <maven.compiler.release>8</maven.compiler.release>
</properties>

Gradle:

// sourceCompatibility / targetCompatibility ではなく release を使う。
// compileJava だけに設定すると compileTestJava には効かず、
// テストだけが JDK 17 の API に対してコンパイルされ続ける。
tasks.withType(JavaCompile).configureEach {
    options.release = 8
}

コードは変えません。ソースも実行する JVM もそのままで、javac の指定だけで直ります。

なぜ「あるはずのメソッドが無い」のか

Java 9 で、Buffer のメソッドが共変戻り値になりました。戻り値の型が変わっています。

flip() の戻り値
Java 8BufferByteBufferflip() を持たず、親の Buffer から継承している)
Java 9 以降ByteBufferByteBufferflip() を上書きした)

JVM のメソッド解決は、戻り値の型まで含めた署名で行われます。 名前と引数が同じでも、戻り値が違えば別のメソッドです。

エラーメッセージの末尾を読むと、それが書いてあります。

java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer;
                          ~~~~~~~~~~~~~~~~~~~~ 「ByteBuffer を返す flip」を呼ぼうとした

Java 8 の JVM が持っているのは flip()Ljava/nio/Buffer;Buffer を返す flip)だけです。求められた署名のメソッドが無いので、NoSuchMethodError になります。

-target 8 は「Java 8 の API でビルドする」という意味ではない

  • -source … 使ってよい文法のレベル(ラムダやレコードを許すか)。
  • -target … 出力するクラスファイルの版
  • どちらも、コンパイル対象の API を制限しません。

つまり -source 8 -target 8 でビルドすると、javac手元の JDK(たとえば 17)のクラスライブラリを見ながらコンパイルします。ByteBuffer.flip() は JDK 17 のものとして解決され、ByteBuffer を返す署名がクラスファイルに焼き込まれます。クラスファイルの版だけが 8 になっているので、Java 8 の JVM は読み込めてしまい、実行してその行に来た瞬間に落ちます

--release 8 は違います。Java 8 当時の API 一覧に対してコンパイルします。だから flip()Buffer を返すものとして解決され、Java 8 の JVM で正しく見つかります。

実際に javap でバイトコードを覗くと、呼び出し先が違うことが分かります。

# -source 8 -target 8 でビルド
invokevirtual  // Method java/nio/ByteBuffer.flip:()Ljava/nio/ByteBuffer;

# --release 8 でビルド
invokevirtual  // Method java/nio/ByteBuffer.flip:()Ljava/nio/Buffer;

同じソースから、違うメソッドを呼ぶクラスファイルが出ています。

なぜ古い JVM で出るのか

落ちるのは、古い JVM の上でだけです。

-source 8 -target 8 でビルドしたクラスを Java 17 で実行すると、正常終了しますByteBuffer を返す flip() が実在するからです。落ちるのは、そのクラスファイルを Java 8 の JVM に持って行ったときだけになります。

  • 開発機は JDK 17。ビルドも通る。動作確認も通る。
  • 本番(または一部の顧客環境、古い CI ランナー)だけが Java 8。そこで初めて落ちる。

-source 8 のときだけ出る警告

-source 8 -target 8 でビルドすると、javac はこう言います。

warning: [options] bootstrap class path not set in conjunction with -source 8

意訳すると「-source 8 と指定されたが、参照するクラスライブラリは 8 のものが指定されていない」です。コンパイラは、手元の JDK のクラスライブラリを見てコンパイルしていることを申告しています

--release 8 にすると、この警告は出ません。警告が消えることが、API まで 8 に揃ったことの目印になります。

切り分け

  • javac の指定を --release に直したのに、まだ落ちる → 依存ライブラリ側が -target でビルドされている可能性があります。その jar は自分のビルド設定では直せないので、Java 8 に対応した版のライブラリを探すことになります。ただし at ... の行だけでは犯人を特定できません。 そこに出るのは「呼び出した場所」であって、「欠けたメソッドを要求しているバイナリの持ち主」ではないからです(自分のコードがライブラリの欠けたメソッドを呼んだ場合も at App.main になります)。読むのはエラー本文の 所有クラス.メソッド(シグネチャ) のほうで、そのクラスを含む jar を依存ツリーから探してください。
  • NoSuchMethodError だが、クラスが java.nio ではない → 原因の型は同じです。新しい JDK でビルドし、古い JVM で実行しているなら、まず --release を疑ってください。List.of(Java 9 追加)のような新しい API を使っていれば、そもそも Java 8 では動きません。--release 8 にすると cannot find symbol でコンパイル時点で弾かれるので、実行時ではなくビルド時に気づけます(確認しました)。
  • エラーが UnsupportedClassVersionError: class file version ... → 別のエラーです。そちらはクラスファイルの版そのものが新しすぎて、JVM が読み込めていません。-target も付けずにビルドした場合がこれです。UnsupportedClassVersionError: class file version 61.0 の直し方 を参照してください。**NoSuchMethodError は「読み込めたが、中で呼んでいるメソッドが無い」**という一段先の失敗です。
  • エラーが NoClassDefFoundError: javax/xml/bind/... → これも別件で、Java 11 が JDK から削除したパッケージの話です。NoClassDefFoundError: javax/xml/bind/JAXBException の直し方 を参照してください。
  • そもそも Java 8 を捨てられないのか → 捨てられるなら、それがいちばん確実です。--release は「古い JVM で動かし続ける」ための指定なので、実行環境を上げられるなら、そちらを先に検討してください。

検証環境

  • eclipse-temurin:17-jdk、ネットワーク有り。同じイメージに openjdk-8-jre-headless(1.8.0_492)を入れ、ビルド用の JDK 17 と実行用の JVM 8 を同居させています
  • 再現:javac -source 8 -target 8 でビルドしたクラスを Java 8 で実行し、NoSuchMethodError: java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer; が出て終了コード 1
  • 修正:同一のソースjavac --release 8 でビルドし直し、同じ Java 8 の JVM で実行して終了コード 0・シグネチャ消滅。reproduce/fix/.java に差分はありません(変えたのは javac の指定だけです)
  • javap で確認した呼び出し先の違い(flip:()Ljava/nio/ByteBuffer;flip:()Ljava/nio/Buffer;)、同じクラスが Java 17 では正常終了すること、clear / position / limit / rewind / CharBuffer.flip も同じ形で落ちること、-source 8 のときだけ bootstrap class path not set の警告が出ること、-target 8 だけを指定するとコンパイル自体が通らないこと、--release 8 では List.ofcannot find symbol で弾かれることは、いずれも個別に実行して確認しました

Maven の maven.compiler.release / Gradle の options.release は、機構が同じ(コンパイル対象の API を 8 に制限するか)ことを根拠にした対処であり、ビルドツール経由での再現は通していません。

検証(machine-verified)

この修正は eclipse-temurin:17-jdk のバージョン固定コンテナ内で再現し、修正後に java.lang.NoSuchMethodError: java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer; のシグネチャが消えることを機械で確認しています。

verify — run-case.mjs
$ node run-case.mjs jvm/nosuchmethod-bytebuffer-flip
● reproduce java.lang.NoSuchMethodError: java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer; present ✓
● apply fix exit 0
● re-run java.lang.NoSuchMethodError: java.nio.ByteBuffer.flip()Ljava/nio/ByteBuffer; gone ✓
PASS verified · eclipse-temurin:17-jdk · signature gone