2026年2月23日月曜日

【Java進化史 第25回】Java9って、あったっけ? 〜 なぜ話題にならなかったのか 〜

【Java進化史 第25回】Java9って、あったっけ? 〜 なぜ話題にならなかったのか 〜

このブログで、Java8をあらためて勉強した。

ラムダ。 Stream。 Optional。

書き方そのものが変わっていた。

50代エンジニアが、若手のコードを読めなくなる状況を作った元凶だと知った。

では、その次は?

Java9。

……正直、印象が薄い。

あったっけ?


■ なぜ記憶が薄いのか

理由はいくつかありそうだ。

  • LTSではなかった
  • すぐにJava11が来た
  • 現場が積極的に採用しなかった

企業の多くは、Java8のまま様子を見た。

気がつけば、次はJava11。

Java9は、通過点のように見えた。


■ 革命ではなかった

Java8は「書き方」を変えた。

だがJava9は、そこまで派手ではない……らしい。

新しい構文が増えたわけでもない……ようだ。


■ モジュール? なんじゃそれ

Java9で、モジュールなるものが登場したらしい。

モジュール。

なんじゃそれ。

聞き慣れない。

なんとなく、 COBOLとかの世界に出てきそうな単語だ。

それがJavaに入ったという。


■ 本当に地味だったのか

派手ではない。

細かい改善は、いくつもあるらしい。

  • コレクション生成の簡略化
  • Stream APIの拡張
  • Optionalの強化
  • interfaceのprivateメソッド

名前だけ見ると、 正直あまりピンとこない。

本当に何もなかったのか。

それとも、 ちゃんと意味のある進化だったのか。

少しずつ、見てみよう。



【Java進化史 第24回】Java8で日付APIは何がどう変わったのか 〜 DateとCalendarからの卒業 〜

【Java進化史 第24回】Java8で日付APIは何がどう変わったのか 〜 DateとCalendarからの卒業 〜

風の噂で聞いた。

Java8で、日付系のAPIが変わったらしい。

「また何か増えたのか?」

正直、最初はその程度の認識だった。

だが調べてみると、これは単なる追加ではない。

DateとCalendarからの、事実上の世代交代だった。


■ なぜ変更されたのか(軽く)

Java7以前、日付といえばこれだった。


java.util.Date
java.util.Calendar
java.text.SimpleDateFormat

動く。

だが、扱いづらい。

  • DateやCalendarはmutable(変更可能)
  • SimpleDateFormatはスレッドセーフではない
  • 月が0始まりなど、直感的でない仕様

そこでJava8で追加されたのが、 java.time パッケージである。


■ 何がどう変わったのか

① 現在日時の取得

Java7


Date now = new Date();

Java8


LocalDateTime now = LocalDateTime.now();

何が変わったか。

Dateは「日付と時刻の塊」だった。

LocalDateTimeは「日付+時刻」という意味が明確である。


② 日付だけ取得

Java7


Calendar cal = Calendar.getInstance();
int year = cal.get(Calendar.YEAR);

Java8


LocalDate today = LocalDate.now();
int year = today.getYear();

フィールド定数が不要になった。 読みやすさが違う。


③ 日付加算

Java7


Calendar cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_MONTH, 3);

Java8


LocalDate result = LocalDate.now().plusDays(3);

plusDays。 何をしているかが、そのまま読める。


④ フォーマット

Java7


SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String str = sdf.format(new Date());

※ SimpleDateFormatはスレッドセーフではない。

Java8


DateTimeFormatter formatter =
        DateTimeFormatter.ofPattern("yyyy-MM-dd");

String str = LocalDate.now().format(formatter);

DateTimeFormatterは不変(immutable)。 安全に使える。


⑤ 文字列から日付へ

Java7


SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = sdf.parse("2026-02-22");

Java8


LocalDate date = LocalDate.parse("2026-02-22");

APIが自然になった。


⑥ 日付の差分

Java7


long diff = date2.getTime() - date1.getTime();
long days = diff / (1000 * 60 * 60 * 24);

ミリ秒計算を自分でやる必要があった。

Java8


long days = ChronoUnit.DAYS.between(date1, date2);

意図が明確になった。


⑦ タイムゾーン

Java7


TimeZone tz = TimeZone.getTimeZone("Asia/Tokyo");
Calendar cal = Calendar.getInstance(tz);

Java8


ZonedDateTime now =
        ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));

日付・時刻・タイムゾーンが一体として扱える。


■ 結局、何が変わったのか

  • mutable → immutable
  • 曖昧 → 役割分離
  • 計算ベース → 意味ベースAPI

書きやすくなった。

それだけではない。

バグが入りにくくなった。

地味に見える。

だが実務では、かなり大きい進化だ。




2026年2月22日日曜日

【Java進化史 第23回】Java8でBase64は何がどう変わったのか 〜 地味だが確かな進化 〜

【Java進化史 第23回】Java8でBase64は何がどう変わったのか 〜 地味だが確かな進化 〜

風の噂で聞いた。

Java8でBase64が正式に追加されたらしい。

「いや、前からあっただろう?」

そう思った人も多いはずだ。

確かに、Java7以前でもBase64は使えていた。


// Java7以前(内部API)
import sun.misc.BASE64Encoder;
import sun.misc.BASE64Decoder;

// 内部実装。公式仕様ではない
BASE64Encoder encoder = new BASE64Encoder();
String encoded = encoder.encode("Hello".getBytes());

使えてはいた。

しかし、sun.* は内部API。

将来の保証はない。

そしてJava8で、正式APIが追加された。

■ Java8で追加されたBase64


import java.util.Base64;

String original = "Hello Java8";

// 通常のBase64エンコード
String encoded = Base64.getEncoder()
                       .encodeToString(original.getBytes());

// デコード
String decoded = new String(
        Base64.getDecoder().decode(encoded)
);

java.util.Base64。

内部APIから、正式仕様へ。

これが最大の変化だ。

■ 何がどう変わったのか

① 内部API → 標準APIへ

  • Java7以前:sun.misc(内部API)
  • Java8以降:java.util.Base64(正式サポート)

安心して使える。

地味だが、これは大きい。

② 用途別に整理された


// 改行なし(一般的な用途)
Base64.getEncoder();

// URL安全(+ と / を置換)
Base64.getUrlEncoder();

// MIME形式(76文字ごとに改行)
Base64.getMimeEncoder();

以前は用途の区別が曖昧だった。

Java8では、意図がAPIに現れている。

③ 改行仕様が明確になった

  • 通常Encoder:改行しない
  • MIME Encoder:76文字ごとにCRLF改行

メール添付や一部仕様では、 この違いが重要になる。

④ 例外仕様が整理された


try {
    Base64.getDecoder().decode("不正な文字列");
} catch (IllegalArgumentException e) {
    // 不正なBase64文字列
}

decode時の挙動も明確だ。

■ 地味だが、いい進化

Java8はラムダやStreamが目立つ。

しかしその裏で、 こうした基盤の整備も行われていた。

派手ではない。

だが確実に実務を支える変更だ。

地味だが、いい進化だなぁと思う。




【Java進化史 第22回 後編】defaultメソッドの注意点 〜 どこまで“クラス化”したのか 〜

【Java進化史 第22回 後編】defaultメソッドの注意点 〜 どこまで“クラス化”したのか 〜

前回、インターフェースに実装を書くという事実に、 正直な違和感を覚えた。

今回は、少し冷静に整理してみる。

defaultメソッドは、 どこまで許されているのか。

■ defaultメソッドは何ができるのか

まずは単純な例から。


public interface Sample {

    int VALUE = 10; // 定数は持てる(public static final)

    int calc(int x);

    default int doubleCalc(int x) {
        return calc(x) * VALUE;
    }
}

ここで注目すべき点は2つある。

  • インターフェースは定数(public static final)しか持てない
  • defaultメソッドは他の抽象メソッドを呼び出せる

つまり、 defaultメソッドは「振る舞いの共通化」はできる。

しかし、 状態は持てない。

インスタンスフィールドは宣言できない。 コンストラクタもない。

ここは、クラスとは決定的に違う。

defaultメソッドは、 クラスの代わりではない。

あくまで、 契約の延長線上にある“補助的な実装”だ。

■ 多重継承はどうなるのか

では、インターフェースが複数あったらどうなるのか。


interface A {
    default void hello() {
        System.out.println("A");
    }
}

interface B {
    default void hello() {
        System.out.println("B");
    }
}

class C implements A, B {
}

これはコンパイルエラーになる。

どちらのhelloを使うのか、 曖昧だからだ。

Javaは、この曖昧さを許さない。

必ず、実装クラス側で明示的に解決する必要がある。


class C implements A, B {

    @Override
    public void hello() {
        A.super.hello();
    }
}

このように、 どのインターフェースの実装を使うかを 明示的に指定する。

「なんとなく動く」は許されない。

■ 原則は壊れていない

インターフェースは、今でも状態を持たない。

完全なクラスになったわけではない。

defaultメソッドは、 無制限の自由ではない。

制御された拡張である。

だからこそ、 長年守られてきた設計思想は、 完全に崩れたわけではない。

むしろ、 後方互換性を守りながら進化するための、 現実的な選択だったと言える。

違和感はある。

だが、無秩序ではない。

そこが、Javaらしさなのかもしれない。




【Java進化史 第22回 前編】Java8 〜 インターフェースに実装を書く?defaultメソッドはアリなのか 〜

【Java進化史 第22回 前編】Java8 〜 インターフェースに実装を書く?defaultメソッドはアリなのか 〜

若手のコードを見て、手が止まった。


public interface Calculator {

    int add(int a, int b);

    default int subtract(int a, int b) {
        return a - b;
    }
}

class SimpleCalculator implements Calculator {

    @Override
    public int add(int a, int b) {
        return a + b;
    }
}

ここでの読み方はこうだ。

add は従来どおり「実装必須」の抽象メソッド。

一方、default が付いた subtract は、 インターフェース側にあらかじめ実装が用意されている。

実装クラスが独自に書かなければ、 そのままこの処理が使われる。

つまり、インターフェースが 「振る舞いの雛形」を持っている、ということになる。

あれ?

実装していいの?

インターフェースに、処理が書いてある。

しかも、実装クラスでは add しか書いていない。 subtract は自動的に使えてしまう。

正直に言えば、違和感しかなかった。

インターフェースとは「契約」だ。

何をするかを定義する場所であって、 どうやるかを書く場所ではない。

少なくとも、私たちはそう教わってきた。

■ なぜ実装を書かなかったのか

理由は単純だ。

責務を分けるためである。

  • インターフェース = 振る舞いの定義(契約)
  • クラス = 具体的な実装

役割を分離することで、 設計は明確になり、 依存関係も整理される。

そして何より、 カプセル化が守られる。

■ カプセル化と保守性

実装の詳細はクラス内部に隠される。

外部から見えるのは、 メソッドのシグネチャだけ。

だから、保守性と利用者視点の可読性が守られていた。 実装が隠蔽されていることで、変更の影響範囲は実装クラス内部に閉じ込められる。 利用者は契約(インターフェース)だけを読めばよく、それが長年Javaの安定性を支えてきた。

ただし、これは「半分は真実」である。

確かに利用者にとっては読みやすい。 しかし、実装を探すためにクラスを辿らなければならない場面も多かった。

設計が崩れれば、 インターフェースはただの形式だけになってしまう。

■ それでも守られてきた原則

それでも、 「インターフェースに実装を書かない」 という原則は長く守られてきた。

それは単なる文法上の制約ではない。

Javaという言語が大規模開発で信頼されてきた、 ひとつの思想だった。




2026年2月21日土曜日

【Java進化史 番外編】Java8で「符号なし」が追加?本当か?

【Java進化史 番外編】Java8で「符号なし」が追加?本当か?

そういえば。

Java8で「符号なし」が導入された、と どこかで聞いたことがある。

……え?

Javaって、unsigned無かったよね?

なんで今頃?

ちょっと気になったので、調べてみた。


■ きっかけは、IPアドレス

昔、こんなコードを書いたことがある。


InetAddress addr = InetAddress.getLocalHost();
byte[] ip = addr.getAddress();

System.out.println(ip[0]);

出力:


-64

……は?

IPアドレスって、192とか168じゃなかったか?

なぜマイナスになる?


■ 192って何なのか

例として、


192.168.0.1

この「192」は、 IPアドレスの最初の1バイト(8ビット)である。

IPv4は32ビット。


8ビット × 4 = 32ビット

つまり、IPアドレスは4つのbyteでできている。

192を2進数にすると:


11000000

これが1バイトの正体だ。


■ なぜ -64 になるのか

Javaのbyteは符号付き。


-128 ~ 127

11000000を符号付きとして解釈すると、 それは -64 になる。

それだけの話である。


■ Java8ならどう書く?

IP取得方法は変わっていない。 InetAddressも昔のままだ。

変わったのは、読み方である。


InetAddress addr = InetAddress.getLocalHost();  
byte[] ip = addr.getAddress();                  

int first = Byte.toUnsignedInt(ip[0]);

System.out.println(first);

Java8で追加された Byte.toUnsignedInt() によって、 符号なしとして読むことができるようになった。


■ 4つ並べると


for (byte b : ip) {
    System.out.print(b + ".");
}

-64.-88.0.1.

Java8


for (byte b : ip) {
    int value = Byte.toUnsignedInt(b);
    System.out.print(value + ".");
}

192.168.0.1.

■ で、結局なにが起きたのか

Java8は、unsigned型を追加したわけではない。

unsignedとして「読む方法」を追加した。

byteの仕様は変わっていない。 intの仕様も変わっていない。

もちろん、boxingやラッパークラスの挙動もそのままである。

既存のコードを一切壊さない。

その範囲でできることを追加した。

値そのものは昔から同じ。

型は増やさず、 解釈の手段だけを加えた。


■ なぜ今頃?(少しだけ補足)

Javaは当初、型を増やさない設計を選んだ。

だからunsignedは入らなかった。

だが、 ネットワークやビット演算の現場では、 0~255として扱いたい場面が多かった。

そこでJava8で、 型は増やさず、解釈メソッドを追加した。

いかにもJavaらしい、 後方互換を守る現実解である。



【Java進化史 第21回 】Java8 〜 ::(メソッド参照)とは何か。どう読めばいいのか〜

【Java進化史 第21回 】Java8 〜 ::(メソッド参照)とは何か。どう読めばいいのか〜


若手のコードを見た。

list.forEach(System.out::println);

「:」なら知っている。

でも「::」は知らない。

Javaに、こんな記号あったか?

どこが引数で、どこが処理なのか。

まずは、一度戻る。



■ 昔のJavaに戻る

for (String s : list) {
    System.out.println(s);
}

これなら分かる。


■ 次にラムダに戻る

list.forEach(s -> System.out.println(s));
s -> System.out.println(s)

左側が「受け取る引数」。

右側が「実行する処理」。

s を受け取り、println(s) を実行する。


■ 何が省略されたのか

System.out.println(s)

受け取った s を、そのまま println に渡しているだけ。

だから省略できる。


■ ここで :: が出てくる

System.out::println

これは、

s -> System.out.println(s)

を短く書いただけ。

まだ実行していない。

メソッドそのものを渡している。


■ A::B の読み方

A::B

「A の B というメソッドを使う」

A.B() ではない。

呼び出しではなく、 メソッドそのものを渡している。


■ 他のパターンも同じ

① 静的メソッド

list.stream()
    .map(s -> Integer.parseInt(s));
list.stream()
    .map(Integer::parseInt);

Integer クラスの parseInt メソッドを使う、 という意味。


② 特定オブジェクトのメソッド

Supplier<String> supplier =
    () -> user.getName();
Supplier<String> supplier =
    user::getName;

user の getName メソッドを使う。


③ コンストラクタ参照

Supplier<List<String>> supplier =
    () -> new ArrayList<>();
Supplier<List<String>> supplier =
    ArrayList::new;

ArrayList のコンストラクタを使う、 という意味。

これも同じ。

「new を呼び出している」のではなく、

「このコンストラクタを使ってね」と渡している。


■ もう一度まとめる

昔のJava。

for (String s : list) {
    System.out.println(s);
}

ラムダ。

list.forEach(s -> System.out.println(s));

メソッド参照。

list.forEach(System.out::println);

意味は同じ。

段階的に省略されているだけ。


■ 何が変わったのか

昔のJavaは、 「全部書け」だった。

Java8からは、 「同じ意味なら短くしていい」に少し進んだ。

:: は、その象徴。

でも中身は昔と変わらない。



【Java進化史 第25回】Java9って、あったっけ? 〜 なぜ話題にならなかったのか 〜

【Java進化史 第25回】Java9って、あったっけ? 〜 なぜ話題にならなかったのか 〜 このブログで、Java8をあらためて勉強した。 ラムダ。 Stream。 Optional。 書き方そのものが変わっていた。 50代エンジニアが、若手のコードを読...