← melang

Java

melangの各noteで他言語と比較した、Javaの設計をまとめたページ。ここに書かれている内容は、各noteの比較表から自動で集めたものです。

型

型注釈を書く場面が比較的多い。

int age = 20;
int age = 20;
String name = "Alice";

ローカル変数の型推論(`var`)はJava 10以降で使えるが、基本的には型を明示して書くスタイル。

型注釈の位置

型が変数名より前に来るprefix方式。推論はローカル変数の`var`に限定。

int age = 20;
int age = 20;
var name = "Alice"; // Java 10以降、ローカル変数のみ推論できる

String greet(String name) { // 引数・戻り値は常にprefixで明示
    return "Hello, " + name;
}

`Type name`という前置構文が基本で、フィールド・引数・戻り値すべてに一貫して適用される。`var`はローカル変数宣言にのみ使え、引数や戻り値の型推論は言語仕様として存在しない。

型変換

キャスト構文と、失敗時に例外を投げるAPIを併用する。

int y = (int) x;
long big = 3_000_000_000L;
int a = (int) big; // 黙って切り捨てられる
int b = Math.toIntExact(big); // 範囲外ならArithmeticException

int n = Integer.parseInt("42");
int m = Integer.parseInt("abc"); // NumberFormatException

Number num = 10;
long l = num.longValue(); // メソッド形式の変換もある

基本はキャスト構文で、縮小変換は黙って値を切り捨てる。範囲外を検出したい場合は`Math.toIntExact`のような別のAPIを選ぶ必要がある。文字列のパースは`parseInt`が検査例外ではない`NumberFormatException`を投げるため、失敗可能性がシグネチャに現れない。

暗黙の型変換

拡大変換は暗黙、縮小変換は明示が必要。

int x = 10;
int x = 10;
long y = x; // 暗黙にlongへ拡大変換される

long a = 10;
int b = 20;
long c = a + b; // bがlongへ暗黙変換されてから加算される

short s = (short) x; // 縮小変換にはキャストが必要

intからlong、floatからdoubleのような「情報を失わない」拡大変換(widening)は暗黙に行われる。逆に縮小変換(narrowing)は明示的なキャストを要求する。二項演算子のオペランドも、大きい方の型に暗黙に合わせられる(binary numeric promotion)。

Enum / Sum Type

enumはクラス的な機能を持つが、ADTAlgebraic Data Type(代数的データ型)の略。複数の型を組み合わせて新しい型を作る手法で、「AまたはB」のように取りうる形が閉じた集合になっているものをSum Type(直和型)と呼ぶ。Rustの`enum`やHaskellの`data`が代表例で、各variant(構成要素)が異なるフィールドを持てる点で、単純な整数の列挙とは異なる。はsealed + recordで表現する。

sealed interface Result permits Success, Failure {}
sealed interface Result permits Success, Failure {}
record Success(User user) implements Result {}
record Failure(String message) implements Result {}

String describe(Result r) {
    return switch (r) {
        case Success s -> "ok: " + s.user();
        case Failure f -> "error: " + f.message();
    }; // sealedを網羅しないとコンパイルエラー
}

`enum`はクラスとして状態・メソッドを持てるが、variantごとに異なるフィールド構成を持つADTAlgebraic Data Type(代数的データ型)の略。複数の型を組み合わせて新しい型を作る手法で、「AまたはB」のように取りうる形が閉じた集合になっているものをSum Type(直和型)と呼ぶ。Rustの`enum`やHaskellの`data`が代表例で、各variant(構成要素)が異なるフィールドを持てる点で、単純な整数の列挙とは異なる。ではない。ADTAlgebraic Data Type(代数的データ型)の略。複数の型を組み合わせて新しい型を作る手法で、「AまたはB」のように取りうる形が閉じた集合になっているものをSum Type(直和型)と呼ぶ。Rustの`enum`やHaskellの`data`が代表例で、各variant(構成要素)が異なるフィールドを持てる点で、単純な整数の列挙とは異なる。用途は`sealed interface` + `record`で表現し、switch expressionと組み合わせれば網羅性を検査できる。

値の不変性

`final`は参照の再代入を防ぐだけで、参照先の変更は防がない。

final T x = ...; / record User(String name) {}
final List<String> tags = new ArrayList<>();
// tags = new ArrayList<>();  // コンパイルエラー: 再代入
tags.add("admin");             // OK: 参照先は変更できる

record User(String name) {}    // フィールドはfinal(浅い不変)
var fixed = List.of("a", "b"); // 不変コレクション
// fixed.add("c");             // 実行時にUnsupportedOperationException

`final`は束縛(変数・フィールド)を固定するだけで、参照先のオブジェクトは変更できる。`record`や`List.of`で不変にできるが、選ぶのは書き手である。不変コレクションの違反は、コンパイル時ではなく実行時に例外として現れる。

名前付き引数

名前付き引数はなく、オプションが多い場合はBuilderパターンで代替する。

void foo(int width, int height)
void foo(int width, int height) {}
foo(10, 20); // 引数名は呼び出し側に現れない

new PersonBuilder().width(10).height(20).build(); // Builderで意味を明示

言語機能としての名前付き引数はない。引数が増えた場合はBuilderパターンでメソッド名に意味を持たせることが多い。

関数の引数

すべて値渡し。オブジェクトは参照の値が渡され、中身は共有される。

void f(int n, StringBuilder sb)
void f(int n, StringBuilder sb) {
    n = 1;          // 呼び出し側には影響しない
    sb.append("a"); // 呼び出し側から見える(同じオブジェクト)
}

引数はすべて値渡しだが、オブジェクトの場合は参照の値がコピーされるため、中身を書き換えると呼び出し側から見える。「値渡しなのに参照のように見える」という誤解を招きやすい。渡し方を選ぶ構文はなく、呼び出し側にも印はない。melangの渡し方はこれに近いが、オブジェクトが不変なので、共有された中身を書き換えられることはない。

if 文/式

`if`はstatement。値の分岐には三項演算子`?:`やswitch式を使う。

if (cond) { ... } else { ... } / cond ? a : b
int x = cond ? 1 : 2;   // 三項演算子は式で、elseに相当する側が必須

if (cond) {
    process();
} else {
    handle();
}

`if`はstatementで、値を返せない。値の分岐は三項演算子(両側が必須)かswitch式で書く。「手続きは`if`、値は別の構文」と分かれているが、手続きの`else`は自由に使える。

I/O の明示

checked exceptionの`throws`が、I/Oの可能性を間接的に示す。

String readFile(String path) throws IOException
String readFile(String path) throws IOException {
    return Files.readString(Path.of(path));
}

int square(int x) throws IOException {
    readFile("x"); // 呼ぶなら自分もthrowsを宣言する必要がある
    return x * x;
}

`IOException`はchecked exceptionのため、呼び出し元はcatchするか自身の`throws`に伝播させる必要があり、`io`の伝播規則に近い構造を持つ。ただし目的は例外処理でありeffectの分類ではなく、unchecked例外を投げるI/O API(一部のNIOなど)は対象外になる。

言語名

地名・飲料名に由来する固有名詞で、衝突は少ないが商標管理が絡む。

インドネシアの地名兼コーヒーの俗称に由来する固有名詞で、一般的なプログラミング用語との衝突は少ない。一方で`Java`は当初Sun Microsystems、現在はOracleの商標であり、言語名が単なる識別子ではなく法的に管理された商標である点が、コミュニティ主導のOSS言語とは異なる性質を持つ。