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からは、 「同じ意味なら短くしていい」に少し進んだ。

:: は、その象徴。

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



2026年2月19日木曜日

【Java進化史 第20回 】Java8 〜Optionalとは何か。どう読めばいいのか〜

【Java進化史 第20回 】Java8 〜Optionalとは何か。どう読めばいいのか〜


昔は、こういうコードを何度も書いた。

 User user = findUser(id); if (user != null) { String name = user.getName(); if (name != null) { System.out.println(name); } } 

null を確認する。

また null を確認する。

チェックを忘れれば、 NullPointerException。

仕方なかった。

当時のメソッドは、こうだったからだ。

 User findUser(int id); 

見つからなければ、null を返す。

だから、呼び出す側で毎回確認していた。


■ Java8では、こう書ける

 Optional<User> findUser(int id); 

戻り値が変わった。

null を直接返さない。

値が無い場合は、 「空の Optional」を返す。

読むときは、こう読む。

「User が入っているかもしれない箱」


■ Optionalとは何か

まずは宣言。

 Optional<String> name; 

意味は、

「String が入るかもしれない箱」。

値がある場合。

 Optional<String> name = Optional.of("Taro"); 

中身あり。

値が無い場合。

 Optional<String> name = Optional.empty(); 

中身なし。

これは true / false ではない。

「空の箱」を作っているだけだ。

null かもしれない値を入れる場合。

 Optional<String> name = Optional.ofNullable(value); 

value が null なら empty。

値があれば、中身あり。


■ どう使うのか

中身があるか確認する。

 if (name.isPresent()) { System.out.println(name.get()); } 

isPresent() は boolean を返す。

get() は中身を取り出す。

※ 中身が無いと例外になる。

デフォルト値を使う場合。

 String result = name.orElse("default"); 

中身があればその値。

無ければ "default"。

中身があるときだけ処理する。

 name.ifPresent(n -> System.out.println(n)); 

昔の

 if (user != null) 

に近い。


■ NullPointerExceptionは減るのか

昔はこうだった。

 User user = findUser(id); System.out.println(user.getName()); // NPEの可能性 

Optional では、

 Optional<User> user = findUser(id); 

そのまま user.getName() は書けない。

必ず、

isPresent()、orElse()、ifPresent()

などを通す。

だから、 nullチェック忘れの事故は減る。

ただし、 get() を乱用すれば例外は出る。


■ どこで使うのか

基本は、戻り値。

 Optional<User> findUserById(int id); 

見つからない可能性があるメソッド。

フィールドや引数では、あまり使わない。


■ 何が良くなったのか

「nullを返す」から、

「値が無い可能性があると宣言する」へ変わった。

その結果、

nullチェック忘れの事故が減り、 コードの意図が少し読みやすくなった。




2026年2月16日月曜日

【Java進化史 第19回 】Java8 〜interfaceが壊れた日(@の正体)〜

【Java進化史 第19回 前編】Java8 〜interfaceが壊れた日(@の正体)〜


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

interface に @ が付いている。

しかも、その中に実装らしきものがある。

例えば、こんなコードだ。


@FunctionalInterface
public interface Task {

    void execute();

    default void log() {
        System.out.println("log");
    }
}

@ が付いている。

しかも、実装まである。

え?
interface って、実装書けたっけ?

何が起きているのか、分からなくなった。


■ 混乱は、2つあった

落ち着いて見ると、
混乱は2つあった。

1つ目。
上に付いている「@」。

2つ目。
interface の中にある「実装」。

一気に理解しようとして、止まった。

だから今回は、
まず「@」のほうだけ整理する。


■ @FunctionalInterface が出てきたら

正直、「関数型」という言葉は難しい。

だから私は、こう読むことにした。

「この interface は、メソッドが1つだけです」

それだけだ。


@FunctionalInterface
public interface Converter<T, R> {
    R convert(T input);
}

見るべきなのは、ここ。

抽象メソッドが1つだけ。

もし2つに増やすと、
コンパイルエラーになる。

つまりこれは、

「1メソッドで使います」

という宣言だ。


■ なぜ、わざわざ宣言するのか

では、なぜそんな宣言が必要なのか。

理由はシンプルだ。

うっかり2つ目の抽象メソッドを 追加してしまうのを防ぐため。

もし増やせば、コンパイルエラーになる。

つまりこれは、 設計のブレを防ぐための安全装置だ。

特別な機能を追加するものではない。

設計の意図を、はっきりさせるための印だ。


■ interface の形は変わっていない

public interface。

メソッド定義。

書き方は昔と同じだ。

@ が付いただけ。

interface 自体が 別物になったわけではない。


■ もう1つの混乱

だが、まだ疑問は残る。

なぜ interface の中に、 実装があるのか。

それは、@ とは別の進化だ。




【Java進化史 第18回】Java8 〜「@」も読めなかった日

【Java進化史 第18回】Java8 〜「@」も読めなかった日〜

若手のコードを見たとき、
固まったことがある。

-> が読めない。
Streamも追えない。

そして、やたらと出てくる「@」。

なんじゃこれ?


■ @ は何者か

調べてみると、
これは「アノテーション」という仕組みだった。

コードに“意味”を追加するための印。
処理そのものを書くものではない。


@Override
public String toString() {
    return name;
}

「ちゃんとオーバーライドしていますよ」
という宣言だ。

もしメソッド名を間違えれば、
「それ、オーバーライドになっていませんよ」と
コンパイル時に教えてくれる。

人間のミスを減らすための仕組みだ。


■ いつからあったのか

アノテーションは、Java5から存在している。

つまり、
私が気づかなかっただけだ。

だがJava8で、
その世界は少し広がった。


■ Javaで使える主なアノテーション一覧(〜Java8)

アノテーション 用途 利用可能バージョン
@Override 正しくオーバーライドしていることを示す Java5〜
(※Java6からインターフェースにも適用可)
@Deprecated 非推奨であることを示す Java5〜
@SuppressWarnings コンパイラ警告を抑制する Java5〜
@SafeVarargs 可変長引数の安全性を保証する Java7〜
@FunctionalInterface 関数型インターフェースであることを示す Java8〜
型アノテーション
(@Target(TYPE_USE))
型そのものにアノテーションを付けられる Java8〜

こうして並べてみると、
Java8で増えたものがある。

@FunctionalInterface と、
型アノテーションだ。


■ 型にも付くようになった

Java8から、
アノテーションは「型」にも付けられるようになった。


List<@NotNull String> names;

これまでは


@NotNull List<String> names;

こうだった。

前者は「namesがnullではない」。
後者は「リストの中のStringがnullではない」。

守れる範囲が、少し細かくなった。


■ 読めなかった理由

若手は、これを普通に読む。

私は止まった。

-> と同じくらい、
@ も読めなかった。

だが分かってしまえば、
やっていることはシンプルだ。

コードに意味を足しているだけ。

Java8はラムダだけではない。
@ の世界も、静かに広がっていた。


■ 次回へ

Java8で追加された @FunctionalInterface。
これはラムダと深く関係している。

Javaはどこまで「関数型」に寄ったのか。
次回、そこを見ていく。





2026年2月15日日曜日

【Java進化史 第17回】Java8 〜データが壊れた日。forEachが危ない理由〜

【Java進化史 第17回】Java8 〜データが壊れた日。forEachが危ない理由〜


昔、データが壊れたことがある。

テストでは動いていた。
だが本番で崩れた。

件数が合わない。
値が欠ける。
ときどき例外も出る。

原因は、スレッドセーフではないクラスだった。


■ スレッドセーフとは

複数のスレッドから同時に触っても、 データが壊れないこと。

逆に言えば、 同時に触ると壊れるクラスもある。

そして怖いのは、

テストでは再現しないことがある。

タイミング次第だからだ。


■ 並列処理は、昔は怖かった

Java7まで、 並列処理を書くには自分で守る必要があった。

synchronizedを付ける。
Concurrent系を使う。
共有変数に気を付ける。

だから私は、 並列処理が少し怖かった。


■ Streamでも同じことが起きる

List<String> result = new ArrayList<>();

list.parallelStream()
    .map(Employee::getName)
    .forEach(result::add);   // これ

parallelStreamにすると、 複数スレッドが同時にaddする可能性がある。

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

つまり、 また壊れる可能性がある。


■ ああ、だからforEachは危ないのか

forEachは、 外の変数を触る。

外を触るということは、 共有するということだ。

共有は、並列では危ない。


■ collectという書き方

List<String> result =
    list.parallelStream()
        .map(Employee::getName)
        .collect(Collectors.toList());

collectは、 外の変数を使わない。

だからparallelでも、 安心して書ける。

仕組みの詳しい話までは知らなくていい。

並列にするなら、collectを書く。


■ Stream編、ここまで

最初は、 若手の1行が読めなかった。

そこから、

  • 左から読む
  • 何をやるかを書く
  • forEachは危ないことがある

ここまで来た。

もう、Streamは怖くない。

Java8は、 少しだけ味方になった。




【Java進化史 第16回】Java8 〜Streamは「どうやるか」ではなく「何をやるか」〜

【Java進化史 第16回】Java8 〜Streamは「どうやるか」ではなく「何をやるか」〜

前回は、Streamを「左から読む」練習をしました。

filter → map → forEach。

左から順番に読めばいい。

それだけで、若手の1行が読めるようになる。



でも、まだモヤモヤが残る

なぜ、こう書くのか。

Java6までの自分には、 for文のほうが自然でした。

for (Employee e : list) {
    if (e.isActive()) {
        System.out.println(e.getName());
    }
}

順番も、処理も、全部自分で書いている。

安心感があります。


Streamは「何をやるか」だけを書く

list.stream()
    .filter(e -> e.isActive())
    .map(e -> e.getName())
    .forEach(System.out::println);

このコードをよく見ると、 回し方は書いていません。

  • どこから回すか
  • どうやって回すか
  • 順番をどう制御するか

何も書いていない。

書いているのは、

  • 条件で絞る
  • 名前に変換する
  • 出力する

「何をするか」だけです。


動きは1件ずつ“かもしれない”

Streamは内部で、

要素1 → filter → map → forEach
要素2 → filter → map → forEach
...

のように流れていきます。

でも、それを意識しなくていい。

1件ずつ処理するかもしれないし、 並列になるかもしれない。

自分は「どうやるか」を考えなくていい。

これが、Streamの価値です。


オブジェクト指向の延長だった

Javaは昔から、 実装を隠してきました。

Collectionもそう。

インターフェースで定義し、 中身は隠す。

Streamも同じです。

処理のやり方は任せる。

自分は、やりたいことを書く。

そう考えると、 少しだけ腹に落ちました。


次回予告

次回は、collectとforEachの違い。

「結果を返す」のはどちらなのか。

ここが分かると、Streamは完成します。




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

【Java進化史 番外編】Java8で「符号なし」が追加?本当か? そういえば。 Java8で「符号なし」が導入された、と どこかで聞いたことがある。 ……え? Javaって、unsigned無かったよね? なんで今頃? ちょっと気になったので...