BambTech

コンピュータサイエンスの学習記録等です。

近況整理(仕事編)

この1,2ヶ月で結構大きな決断をプライベートと仕事で1つずつしたから、整理してみる。
これはその仕事編の整理

wamiota.hatenablog.com

これは何

COVID の渦中だけど、転職をすることになった話。
最終出社日を迎えたから、今の気持ちを整理したい。

現職について

新卒でアクチュアリーと呼ばれる職種の候補生として保険会社に入社した。
いろいろあってエンジニアにジョブチェンジしたいと思い、拾ってもらえたのが今の会社。

最初はいわゆる業務システム的なものの開発をしているチームにジョインすることになった。
ソフトウェアエンジニアリングのソの字もわからない中で、新しいことだらけの毎日だった。この時期がもしかしたら一番大変で一番勉強したかもしれない。
ただ、若手が多いチームだったから、研修のようなものは充実していたように思う。初めてのチームとしてすごくよかったのではないかなと思う。
保険会社はチームビルディングのかけらもなくて、人間関係の階層化が激しいところだったから、最初の頃は全然違う環境に慣れなかった。たくさんのご迷惑もかけてしまったのではないかと思う。

業務システム的なものの開発に1年2ヶ月くらい携わったあとに、別のチームにジョインした。
結果的にこのチームにはこれまでの10ヶ月ほど在籍したことになる。長くはないけど、すごく思い入れのある期間になった。

技術的には、Golang, GraphQL, gRPC, k8s, GraphDB などすごくモダンなものを使って開発をしていた。
自分がメインで担当したモジュールについてはスクラッチでの開発だったし、コードベースのアーキをどう作っていくかにも携わることができた。本当に大きな経験だった。
あと、ステークホルダーを意識した仕事の進め方や、タスクやモジュールに対してオーナーシップを取ることも少しはできるようになったかなと思う。

このチームでは、自分からチームビルディング的なことにも率先して取り組んだつもり。
どうすればチームメンバーが働きやすい環境になるか、チームとして進捗を出すにはどうすればいいかという部分も自分なりに必死に考え、実践する経験にもなった。

また、チームメンバーが自分以外海外出身の方々だったから、文面でのやり取りは基本英語を徹底するようにできたのもよかった。
MTG まで英語はちょっと踏み出せなかったけれど、ネイティブもいる中でいろいろな英語の使い方を学べたり、英語で伝えるにはどうすればいいかを考えられたのはすごくよかった。
リモートの中、チームの Slack Channel は発言・相談・雑談をしやすい場として存在していたし、欧米のノリ的なものも感じることができてすごく楽しかった。
これからは普段から英語を書かなくなるから、なんとか英語学習を継続できるよう有給消化中にどうするか考えたい。

現職はすごく居心地が良くて、多くのことを学べた場でもあって、本当に毎日楽しく働くことができた。仕事したくないって思った日を数えるのが難しいくらい。
いろいろな価値観に出会って、いろんな勉強をさせてもらって、自分の考えも大きく変わった2年間だった。間違いなく自分の人生の大切な期間の一つになると思う。
また、そう思える会社で働くことができた自分はすごく恵まれているし、幸せだと思う。本当に感謝しかない。

転職のきっかけ

ガッツリ転職活動していたわけではなくて、話をいただければ聞く程度だった。 結果的に転職することになったけど、理由としては2つ。

  1. 会社の創業期から、一つの組織・プロダクトを作り上げていくフェーズに挑戦したいと思った
  2. 世界を相手にしたプロダクトの開発に携わって、いずれは自分も海外に出ていきたいと思っていた

現職は今はもう結構大きな会社で、細かい文化はチームによって別れているけど、全体的な方針等は持っているし、ソフトウェア開発も多くのプロジェクトが並行で走っている(文化に関してはすごく柔軟だった)。 世の中での認知度もかなりのものだと思う。
抽象的な社内文化・制度ができていくフェーズで自分の考えを発信したり、いろいろな考え方を吸収すること、プロダクトの成長フェーズを初期から肌で感じることができるのはスタートアップかなと思った。

現職は出戻り自由を掲げているし、とてもありがたいことにまた戻ってきてほしいと言ってもらえてる。
保険として考えるのはよくないけど、評価してくれていて戻る場所もありそうなので、挑戦したいという自分の今の気持ちを大切したいなと思った。

これから

toB 向けのプロダクト開発に携わることになる。すでにプロダクトはリリースされていて、シリーズAに向かっているようなポジションに当たる(厳密には違うけど)。
元から国外へ出ていくことを目標として作られたプロダクトで、海外に出ていきたいと思っている自分ともマッチしているなと思った。
まだまだ歴は長くないけれど、これまで経験してきたこと、自分の中で深く考えて持っているものを駆使して頑張りたいと思う。

今はまだ入社前だけど、まだ知られていないものを世の中の人たちに知ってもらえるように、試行錯誤していくことを想像すると、不安もある一方で結構ワクワクしてもいる。
これまで以上に自分で考えて動いていかないといけなくなるし、すごく大変だと思うけど、楽しく頑張っていきたい。

近況整理(プライベート編)

この1,2ヶ月で結構大きな決断をプライベートと仕事で1つずつしたから、整理してみる。
これはそのプライベート編の整理

wamiota.hatenablog.com

これは何

COVID の流行に伴い仕事がフルリモートに移行したから、北海道へ移住した話。

流行がおさまって、会社からある程度の出社を要求されるようになったら東京へ戻ることももちろん飲み込んだ上で引越しを決めた。
移住先を決めるときに考えていたこととか、実際に札幌に住んでみて感じていることを書いていこうかなという感じ。
基本的に「北海道はいいぞ」となるけど。

移住先の検討

基本的にはいくつかの条件の元探していった。

  • 東京へのアクセスがいい
  • 車を使わなくても生活できる
  • 2LDK 以上の賃貸に住める

東京へのアクセス

会社が東京にあることを考えて、いざというときにすぐ東京に行けるほうがいいかなと漠然と思っていた。
それ以外に特に理由はない。

車なし

将来的にどこでどんなチャンスに出会えるかわからないし、その時がいつ来るかもわからない中で車だけに限らずそこそこ大きな買い物をするのは嫌だった。
自分で住む場所を選ぶのに、わざわざ車社会の場所に行く必要もあまり感じられなかった。

広い家

東京を出ることを検討した一番の理由がこれ。東京にいた頃は 1K に住んでいて、仕事も勉強もご飯を食べるのもプライベートの時間もすべて10畳程度の狭い空間で行っていた。
真面目に自粛していたから、外に出るのもスーパーへ食料品を買いに行く時間だけだった(1週間のうち総計1時間程度)。
そんな中で結構ストレスを感じていたのか、飲酒量がかなり増え暴食もめっちゃした。公私の区別が全くつかなくて全然勉強も捗らなかった。
このままリモートが続くと身体的にも精神的にもよくないと思って、家の中でも気持ちの切り替えができるように書斎と寝室を持ちたいと思うようになった。

場所の検討

以上の条件を満たしている中から実際に検討に入ったのは、札幌・仙台・富山・金沢・名古屋・京都・福岡あたりかな。

東京に住んでいて、無形のものを含むモノが豊富にあって、ちょっと外に出れば欲しい物が手に入る環境は良いと思っていたから、地方都市でも大きめのところに近い方がよかった。あとは、せっかくだから何回も行ったことのある場所を除外して札幌と福岡が残った。この2つはどっちにするかかなり悩んだ。

最終的には、家賃の安さと雪国に住んでみたいというただの好奇心で札幌に決めた。
ちなみに札幌の家賃は福岡の2/3くらい(東京の1/3くらい)。札幌は広いから一概にはいえないけど、だいたいこんな感じの印象。

実際に住んでみて

住みはじめて早くも1ヶ月たったから、住み始めての感想でもまとめる。真冬しか経験してないから、これから印象が変わっていく可能性もあると思う。

天気

移住の話をした人にはだいたい「寒さは大丈夫?」と心配された。事前に住宅の暖房が異常なほど強いことは知ってたからかなり軽く見ていた。

suumo.jp

実際に暖房はめちゃめちゃ強い。エコモードで運用していないとあっという間に20℃後半とか行く。部屋の中が暑すぎて窓を開けることもたまにあるレベル。

外は寒いことには寒いけど東京みたいに風が強かったりすることがあまりないから、ちょっと寒さの種類が違うと思う。
近くのコンビニならパーカーだけで行くし、ちょっと距離があるスーパーに行くとしてもヒートテックに何かを着て、マウンテンパーカーで全然楽勝。
最高気温が1℃もあればむしろ暑いんじゃないかってくらい。僕が寒さに強いだけかもしれないけど。

天気で辛いのは解けかけの雪の凍結かな。まだまだ歩くのに慣れていないから、たまに滑る。怖い。

モノの利便性

ネットでの買い物は少し不便かなと思う。物理的に関東から遠い上に、冬は雪の影響をもろに受ける可能性がある。
僕の引越荷物は、東北地方の雪の影響で一週間近く遅れてやってきた。

(誤字ってるな)

Amazon も当然翌日にはやってこない。3,4日遅れくらいで届くかな。
それ以外に関しては特に不便は感じてない。電車も結構発達してるし、札幌市街地が栄えてるから結構なんでも買える。
スーパーとコンビニは近くに何軒かあるし、むしろ食料に関しては東京より豊富・安い・美味しいでかなり満足してる。

部屋

2LDK を選んだのは正解だった。リビング・書斎・寝室が分かれているから、気持ちの切り替えもできる。仕事の効率もだいぶ上がったかなと思う。
書斎の窓からは山も見えるし、田舎出身だから何となく落ち着いていい。

キッチンも広くなって、いろいろ整理しやすくなった。
この1ヶ月はちゃんと自炊できていて健康的なものを食べてる。東京にいるときは UberEats 頼みまくってたけど、こっちに来てからは一回も頼んでない。そして痩せた。

これから

将来のことはわからないけど、今は強制的に出社しないといけなくなるか、海外行きがない限りは住み続けたいと思ってる。
各季節で美味しいものを食べたいし、北海道の色んなところに行ってみたいと思ってる。オールシーズン経験した結果やっぱり出ていくか、、となるかもしれないけど。

GraphQL から gRPC 通信を行う

これは何

今話題の GraphQL から、マイクロサービスでよく使われている gRPC プロトコルでの通信基盤を作ってみた話。
GraphQL とか gRPC については以下のリンクで学べばいいと思う。

Web API初心者と学ぶGraphQL - Qiita

いまさらだけどgRPCに入門したので分かりやすくまとめてみた - Qiita

GQL サーバー

今回は Apollo GraphQL で実装した。
Apollo については以下のリンクがいいかな。

世のフロントエンドエンジニアにApollo Clientを布教したい - Qiita

GraphQL サーバーの実装は主に3つの要素が必要になってくる。
1. スキーマ: 型定義のようなイメージ
2. データソース: GraphQL を用いてリクエストを送る先をまとめるもの
3. リゾルバー: レスポンスをスキーママッピングするようなもの
gRPC へリクエストを送る GraphQL では、データソースの書き方でちょっと困ったので書く。

データソース

よくあるデータソースでは(REST API を想定) apollo-datasource-rest にある RESTDataSource を継承しているクラス内に API コールをするメソッドを定義していく。

import { RESTDataSource } from 'apollo-datasource-rest');

class LaunchAPI extends RESTDataSource {
  constructor() {
    super();
    this.baseURL = 'https://api.spacexdata.com/v2/';
  }

  async getAllLaunches() {
    const response = await this.get('launches');
    return Array.isArray(response)
      ? response.map(launch => this.launchReducer(launch))
      : [];
  }
}

こんな感じ(公式チュートリアルから)。このメソッドでは、 GET: https://api.spacexdata.com/v2/launches がコールされることとなる。
ここから apollo-datasource-grpc があるのではないかと予想できるから、調べてみると案の定発見。

github.com

これの通りに実装していくと、 return this.callRPC(0, { args: { id }, meta, rpcName: 'GetMovie' }); のところで、rpcName には関数が来るっぽくて通信できなかった。
今回は、データソースを別の形で実装した。

import { meta } from '../config';
import * as protoLoader from '@grpc/proto-loader';
import * as grpc from 'grpc';

const packageDefinition: any = protoLoader.loadSync('../../proto/hello.proto');
const proto: any = grpc.loadPackageDefinition(packageDefinition).hello_test;

const client = new proto.Hello('localhost:3941', grpc.credentials.createInsecure());

export default (root: any, params: any) => {
    return new Promise((resolve: any, reject: any) => {
        client.GetHello(params, meta, function(err: any, response: any) {
            if (err) {
                return reject(err.details);
            }
            resolve({ message: response.msg });
        });
    });
}

gRPC の proto ファイルを loadPackageDefinition(path).package_name で読み取ると、クライアントを作ってくれて、クライアント起点で定義した rpc を読んでくれるようになるみたい。明記はしないけど、整理するならこれがデータソースになるのかなと思う。今回はリゾルバーに無理やり入れたけど。

実際に動かしてみる

proto ファイルは ./proto 配下にあって、サーバー側は ./serverGolang で gRPC サーバーを実装してある。
それぞれ実行する。クライアントのポートは 3939 でサーバーのポートは 3941 にしてあるから、クライアントの GUI ツールを開いて叩いてみる。 f:id:wamiota:20200704182638p:plain でけた。

Dgraph をいじってみた話

概要

国内ではあまり聞かないけど、海外ではかなり使用例の多い Graph DB のうち、分散システムへの強みを持った Dgraph をいじってみたので備忘録。

Dgraph とは

Graph DB の中でも、スケールや可用性などの分散システムにとってありがたみが多い物になる。一般的に Graph DB といえば neo4j あたりが一番有名な気がしていて、 Dgraph は公式で比較ブログを出している。

Neo4j vs Dgraph - The numbers speak for themselves - Dgraph Blog

2016年にリリースされて、まだまだ若いプロダクトだけど gRPC に対応していたり、GraphQL との相性が良さそうだったり、今後期待?の技術かもしれない。

構成

Dgraph は Alpha Zero Ratel という3つの異なるノードで構成されていて、それぞれのノードがそれぞれの役割を持っている

  • Alpha
    ストレージへのアクセスを管理してくれるノードだと思われる。

  • Zero
    Alpha を管理するノードだと思われる。

  • Ratel
    スキーマを変更したり、クエリを投げたりできる UI を提供するノード。

クラスタ内部の話は ここら辺 に書かれている。どうも内部的には gRPC を用いてクラスタノード間でのやりとりを行っているようだから、外部からも gRPC を用いて処理を行うと相性が良さそう(特に stream )。

チュートリアル

docker image が存在するから、 pull してきてコンテナを立ち上げれば使えるようになる

# 必要なイメージを pull する
docker pull dgraph/dgraph:latest
docker pull dgraph/standalone:v20.03.1

# コンテナを立ち上げる
docker run -it -p 6080:6080 -p 8080:8080 -p 9080:9080 -p 8000:8000 -v ~/dgraph:/dgraph dgraph/standalone:v20.03.1

これで立ち上がる。クラスタに入ってくるデータは ~/dgraph に格納されていく。
これで localhost:8000 で Ratel へアクセス可能となるから、実際にいじっていけばいい。
公式チュートリアル, 対話式チュートリアル

使ってみた感じ

初期のデータベースからグラフモデルは確か存在してて、人間にとってわかりにくいモデルだったから関係モデルが生まれた背景があった気がする。だから、やっぱりスキーマ(グラフ構造)を考えるのがすごく難しい。Ratel では ERD のようなものを確認できるけど、これもやっぱり人間にはわかりにくい。また、あいまい検索をするとなるとどうしてもパフォーマンスが悪くなってしまうから、elasticsearch とかと組み合わせるといいかもしれない。検索性は facets があったりかなりいいと思う(他の graph を知らないけど)。

gRPC で遊んでみた話

仕事で gRPC を本格的に使いそうかつ、昔から興味があったものの実際に手を動かしたことがなかったので、使ってみた。
基本的には 公式チュートリアル に合わせてやった。

gRPC とは

RPC を実現するために Google が開発した通信プロトコルの一つ。サーバー間での高速な通信を実現できるから、マイクロサービスとかで注目されている。
詳しくは gRPCって何? によくまとめられていたから、忘れた時には確認したい。
上記のところでまとめられていない内容として、通信の定義がある。

Simple RPC

クライアントから一つのリクエストがサーバー側へ送信され、一つのレスポンスを待つ状態。いわゆる普通の通信。
f:id:wamiota:20200516121354j:plain

Server-Side Streaming RPC

クライアントから一つのリクエストがサーバー側へ送信され、サーバーとしてはメッセージをシーケンスとして返すことができる。クライアントは、レスポンスをすべてのメッセージがなくなるまで読み取る。
f:id:wamiota:20200516121934j:plain

Client-Side Streaming RPC

サーバーの逆クライアントバージョン。クライアントはメッセージの送信を終えると、サーバー側が全てを読み取りレスポンスを返すまで待ち状態になる。
f:id:wamiota:20200516122124j:plain

Bidirectional Streaming RPC

bidirectional なので、双方向にストリーミングを可能としたもの。上記2つの組み合わせ的な感じ。read stream と write stream は独立で操作されるため、好きな順番で操作可能となっている。例えば、サーバーはレスポンスを書く前にすべてのメッセージを受け取ってもいいし、メッセージを受け取りながらレスポンスを変えしてもいいというもの。
f:id:wamiota:20200516123307j:plain

実際に実装してみた

環境

用意する環境としては、ローカルに作るのもなんか嫌だったから以下の docker-compose 内で走らせることにした。

# Dockerfile
FROM golang:1.13-stretch

SHELL ["/bin/bash", "-c"]
RUN apt update && apt-get install -y vim unzip

WORKDIR /protoc
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v3.12.0-rc2/protoc-3.12.0-rc-2-linux-x86_64.zip
RUN unzip protoc-3.12.0-rc-2-linux-x86_64.zip
RUN ln -s /protoc/bin/protoc /bin/protoc

WORKDIR /go-grpc
ENV GO111MODULE on
RUN go get -u github.com/golang/protobuf/protoc-gen-go
# docker-compose
version: "3.7"
services:
  go-grpc:
    build:
      context: ./
      dockerfile: Dockerfile
    container_name: "go-grpc"
    volumes:
      - ./:/go-grpc
    tty: true
    privileged: true

これ以降、基本的な操作は protocol buffer を書く -> コンパイルする -> 実際に動かしてみるとなる。コマンドは以下の通り。

## compile
protoc --proto_path ./proto/hoge --go_out=plugins=grpc:./pb/fuga file_name.proto

動かしたもの

とりあえず simple rpc さえちゃんと理解できればあとは遊んでみるだけだから、simple rpc だけ備忘として書いておく。

// unary.proto
syntax = "proto3";

package unary;

// サービス名を定義し、リクエストタイプとレスポンスタイプを定義してあげる。
// この例では、リクエストが point でレスポンスが feature となる。
service RouteGuide {
    rpc GetFeature(Point) returns (Feature) {}
}

// リクエストの定義
message Point {
    int32 latitude = 1;
    int32 longitude = 2;
}

// レスポンスの定義
message Feature {
    string result = 1;
}

これをコンパイルすると、unary.pb.go ができるけど、ここの内容は割愛。
次に、サーバー側とクライアント側の準備をしてあげる。

// ./server/main.go

package main

import (
    "context"
    "fmt"
    "log"
    "net"

    pb "go-grpc/pb/unary"

    "github.com/pkg/errors"
    "google.golang.org/grpc"
)

const port = ":50051"

// ここで使用するサーバーの定義みたいな構造体を作る。
type routeGuideServer struct {
    pb.UnimplementedRouteGuideServer 
}

// 実際に、リクエストを取得して、レスポンスを返してあげるメソッド。
func (s *routeGuideServer) GetFeature(ctx context.Context, in *pb.Point) (*pb.Feature, error) {
        // リクエストを取得
    a := in.GetLatitude()
    b := in.GetLongitude()

    log.Printf("get %v and %v from client\n", a, b)

        // 今回は特に何かの処理をするわけではなく、ちゃんと受け取ったよと結果を作ってあげただけ。
    reply := fmt.Sprintf("return recieved two value: %v %v", a, b)
    return &pb.Feature{
        Result: reply,
    }, nil
}

// サーバーを構築する
func setServer() error {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        return errors.Wrap(err, "failed to build server")
    }
    s := grpc.NewServer()
    var server routeGuideServer
    pb.RegisterRouteGuideServer(s, &server)
    if err := s.Serve(lis); err != nil {
        return errors.Wrap(err, "failed to build server")
    }
    return nil
}

func main() {
    fmt.Println("server start...")
    if err := setServer(); err != nil {
        log.Fatalf("%v", err)
    }
}
// ./client/main.go

package main

import (
    "context"
    "log"
    "time"

    "github.com/pkg/errors"

    pb "go-grpc/pb/unary"

    "google.golang.org/grpc"
)

// 送信したいリクエストを作るところ
func request(client pb.RouteGuideClient, a int32, b int32) error {
    ctx, cancel := context.WithTimeout(
        context.Background(),
        time.Second,
    )
    defer cancel()
    testRequest := pb.Point{
        Latitude: a,
        Longitude: b,
    }
    reply, err := client.GetFeature(ctx, &testRequest)
    if err != nil {
        return errors.Wrap(err, "failed to get response")
    }
    log.Printf("get response\n %s", reply.GetResult())
    return nil
}

// クライアントのコネクションを構築してリクエストを最終的に送信しているところ。
func setClient(a int32, b int32) error {
    address := "localhost:50051"
    conn, err := grpc.Dial(
        address,
        grpc.WithInsecure(),
        grpc.WithBlock(),
    )
    if err != nil {
        return errors.Wrap(err, "failed to connect server")
    }
    defer conn.Close()
    client := pb.NewRouteGuideClient(conn)
    return request(client, a, b)
}

func main() {
    a := int32(10)
    b := int32(20)
    if err := setClient(a, b); err != nil {
        log.Fatalf("%v", err)
    }
}

とりあえず、ここまでは理解できたから、続いて他の通信方法についても遊んでみたいと思う。
ちなみに、実際の実行結果は以下の感じ

root@f5cd980e824f:/go-grpc# go run server/unary/main.go
server start...
2020/05/16 03:59:16 get 10 and 20 from client

root@f5cd980e824f:/go-grpc# go run client/unary/main.go
2020/05/16 03:59:16 get response
 return recieved two value: 10 20

授業が始まって1週間がたった

授業が始まって1週間経って1周した感じがあるから感想をまとめてみる

授業について

ご時世もあって基本はオンライン授業かつ録画してくれるから、(仕事面での)予期していなかった会議体なんかへの参加はしやすい。 これに関しては、世間的にはいいことではないけど個人的には助かっている部分が大きい。
教員も課題の提出を別日に設けていたり、いわゆる「出席必須」というものは少ないように感じる。僕が文系出身だから認識齟齬はありそう。

仕事との兼ね合い

ここもリモートのおかげで苦しみは感じていない。必須の MTG が入ったりすると最悪録画を聴けばいいので、質問はできないけどめっちゃ損をする感じはしない。かなり助かってる。 個人的には新しい PJ に関わることになったので、本当にこれはありがたいと感じてる。

授業内容

ここについては賛否両論あると思うけど、個人的にはめっちゃ勉強になってる。ネットワークの細部や DB の細部など、特に Web 系の開発をしていると普段からあまり意識することのない分野の根幹を勉強することができてとても面白い。 世間にはたくさんの DB パフォーマンスチューニングとかネットワーク系の本が出てるけど、そもそもネットワークの標準モデルの OSI 参照モデルとはなんなのか、どういったものがどの層に該当するか(将来的には HTTP/1.1 と 2 の違いや、 IPv4 と 6 の違いなど学べるから楽しみ)とか、DB ってどういう思想のもと作られているかなど、SQL チューニングの根幹を学べてすごく楽しい。他にも普段何気なく使っているファイルシステムシステムコールレベルでの動きとか、浮動小数点の誤差の話とかもすごい楽しくて、いい感じで授業を受けられている。

所感

リモート授業のおかげで匿名で質問できたり、嬉しいこともそこそこあると思う。ただ、教員と直接会って話せないのはそこそこ寂しいかな。
でも、オンライン授業だから録画とかもあって社会人としては授業も受けやすいし、課題を逃したりもしないし助かっていることの方が多い感じ。

変更不可能なコレクション

今日とあるコードを読んでいて JavaunmodifiedMap なるものがでてきたから調べてみた備忘録

変更不可能なコレクション

変更不可能なコレクションとして2つの意味合いを持たせたワードがあるらしい
1つ目は unmodified で、2つ目が immutable
Oracle のドキュメント に使い分けが書いてあって、具体的には以下の文言が書いてある。

- Collections that do not support modification operations (such as add, remove and clear) are referred to as unmodifiable. Collections that are not unmodifiable are modifiable.
- Collections that additionally guarantee that no change in the Collection object will be visible are referred to as immutable. Collections that are not immutable are mutable.

正直これだけ読んでもいまいち意味がわからなかったから実際に手元で動かしてみた。

unmodified

結論から言うと、unmodified はあくまで unmodified とされていて参照をもっている変数からの修正はできないけど、変数持っている参照先のコレクションからは変更可能というものっぽい。うまく言えないけど。

    public void test() {
        List<String> modifiableList = new ArrayList<>();
        modifiableList.add("a");

        System.out.println("modifiableList: " + modifiableList);
        System.out.println("--");

        assertEquals(1, modifiableList.size());

        List<String> unModifiableList = Collections.unmodifiableList(modifiableList);
        modifiableList.add("b");

        boolean exceptionThrown = false;
        try {
            unModifiableList.add("b");
            fail("add supported for unModifiableList!!");
        } catch (UnsupportedOperationException e) {
            exceptionThrown = true;
            System.out.println("unModifiableList.add() not supported");
        }
        assertTrue(exceptionThrown);

        System.out.println("modifiableList: " + modifiableList);
        System.out.println("unModifiableList: " + unModifiableList);

        assertEquals(2, modifiableList.size());
        assertEquals(2, unModifiableList.size());
    }

// 出力結果
// modifiableList: [a]
// --
// unModifiableList.add() not supported
// modifiableList: [a, b]
// unModifiableList: [a, b]
// --

上記コードでは何をしているかと言うと、 modifiableList への参照を unModifiableList が持っていて、unModifiableList からは直接リストへの変更ができないようになっている。 実際のコードを抜粋すると

    static class UnmodifiableList<E> extends UnmodifiableCollection<E>
                                  implements List<E> {
        private static final long serialVersionUID = -283967356065247728L;

        final List<? extends E> list;

        UnmodifiableList(List<? extends E> list) {
            super(list);
            this.list = list;
        }

        public boolean equals(Object o) {return o == this || list.equals(o);}
        public int hashCode()           {return list.hashCode();}

        public E get(int index) {return list.get(index);}
        public E set(int index, E element) {
            throw new UnsupportedOperationException();
        }
        public void add(int index, E element) {
            throw new UnsupportedOperationException();
        }
        public E remove(int index) {
            throw new UnsupportedOperationException();
        }

set や add, remove といったメソッドに対しては無条件で例外を投げるようになっている。もはやこのメソッドを準備しておく意味はあるのだろうか、、

Immutable

僕は eclipse collection の immutable しかいじったことがないから、ここでは eclipse collection でやってみる。

    public void test() {
        List<String> mutableList = new ArrayList<>();
        mutableList.add("a");

        ImmutableList<String> immutableList = Lists.immutable.ofAll(mutableList);
        mutableList.add("b");

        assertNotEquals(mutableList.size(), immutableList.size());
        System.out.println("mutableList: " + mutableList);
        System.out.println("immutableList: " + immutableList);
    }

// 出力結果
// mutableList: [a, b]
// immutableList: [a]

今回は mutable の方に要素を追加しても immutable の方には何の変化もなかった。おそらく、immutable では参照を持っているのではなくもはや新しくコレクションを clone() しているように見えるけど、一応 ImmutableListFactoryImpl あたりを追ってみる。

    @Override
    public <T> ImmutableList<T> ofAll(Iterable<? extends T> items)
    {
        return this.withAll(items);
    }

    @Override
    public <T> ImmutableList<T> withAll(Iterable<? extends T> items)
    {
        if (items instanceof ImmutableList<?>)
        {
            return (ImmutableList<T>) items;
        }
        if (items instanceof List && items instanceof RandomAccess)
        {
            return this.withList((List<T>) items);
        }
        if (Iterate.isEmpty(items))
        {
            return this.empty();
        }
        return this.of((T[]) Iterate.toArray(items));
    }

    private <T> ImmutableList<T> withList(List<T> items)
    {
        switch (items.size())
        {
            case 0:
                return this.empty();
            case 1:
                return this.of(items.get(0));
            case 2:
                return this.of(items.get(0), items.get(1));
            case 3:
                return this.of(items.get(0), items.get(1), items.get(2));
            case 4:
                return this.of(items.get(0), items.get(1), items.get(2), items.get(3));
            case 5:
                return this.of(items.get(0), items.get(1), items.get(2), items.get(3), items.get(4));
            case 6:
                return this.of(items.get(0), items.get(1), items.get(2), items.get(3), items.get(4), items.get(5));
            case 7:
                return this.of(items.get(0), items.get(1), items.get(2), items.get(3), items.get(4), items.get(5), items.get(6));
            case 8:
                return this.of(items.get(0), items.get(1), items.get(2), items.get(3), items.get(4), items.get(5), items.get(6), items.get(7));
            case 9:
                return this.of(items.get(0), items.get(1), items.get(2), items.get(3), items.get(4), items.get(5), items.get(6), items.get(7), items.get(8));
            case 10:
                return this.of(items.get(0), items.get(1), items.get(2), items.get(3), items.get(4), items.get(5), items.get(6), items.get(7), items.get(8), items.get(9));

            default:
                return ImmutableArrayList.newListWith((T[]) items.toArray());
        }
    }

    public static <E> ImmutableArrayList<E> newListWith(E... elements)
    {
        return new ImmutableArrayList<>(elements.clone());
    }

ここら辺くらいまで見ればもうわかる感じ(最後のメソッドだけ ImmutableArrayList にある)。of メソッドではそれぞれの値に対して新しくコレクションを作っていそうで、default では完全にクローンしている。だから、mutableListimmutableList では最終的にアクセスしにいくメモリが全く異なっていて、かつ immutable には変更をするメソッドがそもそも存在していないから変更不可っぽい。

感想

コードまで追いかけると Oracle のドキュメント に書いてあることはしっかり理解できた。僕が知らない時代に unmodified も immutable もできているけど、とりあえず変更不可能っぽい unmodified を作ってみたはいいもののあまりにも使いにくすぎて immutable ができたのかな。まぁ物によってはちゃんと使い分けもできそうだけど。