syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

劇場版ゼータガンダムこそ抹消すべき最悪の黒歴史

Tokyo MXGガンダム再放送でもCMが流れていますが、宇宙世紀ガンダムシリーズのBDが廉価版として再販されることになりました。4Kリマスターではない従来版の再販ということなので、画質は劣るようですが、正直4Kでなくとも必要十分なんじゃないでしょうか。それよりも大して価格が下がってないことのほうがどうかと思います。

それはそうと、ラインナップにあの悪名高きクソ劇場版ゼータが入っていることが気になります。あれは宇宙世紀ではありません。別の世界線黒歴史です。CMであのクソゼータの映像が流れるたびにゲロを吐きそうになります。AGEのように駄作とかいう生易しいレベルではなく、TV版ゼータどころか、我々が愛した宇宙世紀ガンダムシリーズそのものに対する完全な冒涜です。たまに「賛否両論」とか書かれていることがありますが、日本語が読み書きできないのでしょうか。最後まで観ても賞賛できる部分は何一つありません

自分は宇宙世紀シリーズの作品は概ね観ていて、TV版ファースト、劇場版ファースト、ダブルゼータ逆襲のシャアF91Vガンダム、そして富野由悠季監督作品ではない0080、0083、08小隊も好きなんですが、1991年のローカル再放送で「最初に観たガンダム」として、TV版の「機動戦士Ζガンダム」にはとりわけ思い入れが深かったのです。高校生のときにビデオレンタルで最初に見返したのもゼータでした。ガンプラも旧1/144キット、旧HG 1/144(ウェイブシューター)のゼータを組んだことがあります*1。シャープな印象のゼータはもちろん、マッシブなガンダムMk-IIも好きです。

ではなぜ吐き気を催すほどクソ劇場版ゼータが嫌いなのか。ネタバレ前提なので要注意ですが、TV版より先にクソ劇場版を観てしまうことでゼータガンダムが何たるかを誤解してしまう不幸な人、TV版を視聴済みでうっかりクソ劇場版を観てしまって死ぬほどガッカリする不幸な人が一人でも減ることを願い、ここにそのムカつく点を列挙していきます。

劇場版ゼータが大嫌いな理由その1・「永遠のフォウ」のカット

ゼータガンダムのストーリーは、スペースコロニー、月、地球などを舞台に、主人公カミーユ・ビダンの属するエゥーゴ(反地球連邦組織)、敵対するティターンズ(地球連邦の特殊部隊)、第三勢力アクシズ(旧ジオン公国)の三つ巴による複雑な戦争を描いており、劇場版3本に収めるには尺が足りなすぎます。ですが、だからといって何でもかんでもカットしていいわけではない。

カミーユは作中で二度地球に降りているんですが、一度目にホンコン・シティで出会った薄幸の美少女、フォウ・ムラサメと恋に落ちます。しかしフォウはムラサメ研究所(地球連邦所属)の強化人間であり、無くした記憶を取り戻すために巨大兵器・サイコガンダムに乗り、カミーユと戦うことになります。

二度目に地球に降りたとき*2カミーユティターンズキリマンジャロ基地でフォウと再会するのですが、彼女はさらに強化・調整されてカミーユとの出会いすら忘れていました。戦闘の混乱のさなか、最終的に自分の心を取り戻したフォウは、カミーユをかばって戦死します。フォウはファーストガンダムで言うとララァ・スンのような存在でもあるのですが、ファーストのようにオカルトチックなニュータイプ同士の邂逅は描かれないものの、フォウの死はカミーユのその後に暗い影を落とすことになります。「水の星へ愛をこめて」のインストゥルメンタル・バージョンが流れる中、カミーユがフォウの亡骸を抱きながら、クワトロに「シャア・アズナブルに戻る」ことを促すシーンは極めて鮮烈な印象を残しました*3

www.youtube.com

しかしクソ劇場版ではそもそも地球に一度しか降りず、「永遠のフォウ」もなければ「ダカールの日」もありません。ストーリー上重要なターニングポイントや印象的なシーンがバッサリ削られているんです。フォウの死やダカール演説が明確に描かれないなんて、肉の入っていないすき焼きのようなものです。例えばファーストガンダムにおけるミハルやララァの死がカットされたとしても、あなたは黙っていられますか? これを改悪と言わず、なんとするのか。「永遠のフォウ」の重要性に比べたら、アッシマーの活躍なんか本当にどうでもいいんです。TV版には他にも「ハーフムーン・ラブ」「湖畔」といった、各登場人物像を掘り下げる印象的な回が沢山あるんですが、クソ劇場版ではことごとくカットされてしまっています。そもそもゼータを劇場版3本に収めようとする企画自体が無謀なんです。

劇場版ゼータが大嫌いな理由その2・結末の変更

自分があのクソ劇場版ゼータを嫌う最大の理由は、主人公・カミーユの精神崩壊というゼータの物語の終着点とも言える結末を安直に変更してしまったことです。富野監督とは到底思えない采配に、もはや富野は死んだ、と完全に落胆・失望しました*4*5
TV版では、カミーユ・ビダンは戦争のさなか、様々な人々との出会いと死別を体験していきますが、ニュータイプとして感性が強すぎるがゆえに極限まで精神的ストレスを溜め込んでいきます。最終回「宇宙(そら)を駆ける」で、憧れだったエマ・シーン中尉の死に立ち会い、別れを告げるカミーユの表情は穏やかですが悲壮そのものです。ティターンズパプテマス・シロッコが駆る重MSジ・オとの最終決戦では、死んでいった人々(というかカミーユが関わった女性たち)の力を借りてジ・オの動きを封じ、ウェイブライダーで突撃してシロッコを倒すものの、シロッコの怨念により精神を持っていかれます*6*7

戦闘終了後、母艦・アーガマ所属の数少ない生存者として、メタスのパイロットのファ・ユイリィカミーユの幼馴染)は、精神崩壊して自分が誰なのかも分からなくなってしまったカミーユを発見し、その様子に愕然としながらもアーガマに報告。また、戦死したエマ中尉の乗っていたガンダムMk-IIが漂流しているのを見つけて「お前もアーガマに帰りたいのね……」と語りかけます。最後に百式の残骸が流れていき、Fin.のメッセージ。BGMはやはり「水の星へ愛をこめて」。悲劇ではありますが、これ以上ない本当に完璧な結末でした。続編のダブルゼータでは精神崩壊したカミーユをファが献身的に介護します。

しかしクソ劇場版ではカミーユは精神崩壊しません。散々死人を出しまくった戦争の後なのに、ファと宇宙空間でいちゃついて終わりです。しかもそのいちゃつき方とセリフ回しが気持ち悪いのなんの。吐き気がします*8。何なんだこれは。悪夢のような三流のクソシナリオ。何より、カミーユが精神崩壊しないということは、ダブルゼータジュドー・アーシタへのバトンタッチ*9につながらなくなり、さらには間接的に逆襲のシャアにもつながらなくなります。クソ劇場版ゼータは葬り去るべき別の世界線パラレルワールド)であり、黒歴史以外の何物でもありません。他の既存作品をないがしろにしてしまうような自分勝手で横暴な解釈による後付け割り込み作品など、誰が観たいと思うのでしょうか。リセットでやり直しのきくゲームの別ルートのような何か*10に改竄されてしまったばかりか、続編としてガンダムサーガを紡いできた名作の数々に泥を塗りたくったクソ劇場版ゼータ。こんなゴミクズのような作品の、一体どこに魅力があるというのでしょうか。

テッカマンブレードも主人公が最終的にすべてを失なう過酷なストーリーなのですが、だからこそ記憶に残るんです。あれが単なるご都合主義のハッピーエンドだったら、後世に語り継がれる名作になりえたでしょうか。否です。

劇場版ゼータが大嫌いな理由その3・声優の変更

(以下、敬称略)
引退されてしまったファ(松岡ミユキ)は仕方ないにせよ、なぜかフォウ(島津冴子)、サラ(水谷優子)、ロザミア(藤井佳代子)といった重要な登場人物の声がことごとく変えられてしまいました。声優が音響監督に枕営業しているかどうかなんて知ったことではありませんが、そういう噂がたつのもやむなしと思えるくらい、疑念・疑惑の残る気持ち悪い交代劇でした。加えて「音響監督に裏切られた」とかいう醜い言い訳をする富野監督に対する印象は、この時点で最悪になりました。こいつらはクリエイター以前に人間としてどうなのか? カツ(難波圭一)やハヤト(鈴木清信)も、なぜ変えたし、という印象しかありません。決して後任の声優が悪いわけではありませんが、オリジナル声優が存命で現役なのに変える理由が分かりません。特にサラは声優ですらなく、棒演技のド素人(池脇千鶴)を使ったせいで、違和感ばかり気になって話の内容が頭にまったく入ってきませんでした*11。ちなみに棒演技の評判がよほど悪かったのか、サラは第3部でキャストがさらに変更されています(島村香織)。音響監督のポンコツ無能ぶりがうかがえます。結局、ある意味で池脇氏も犠牲者のうちの一人だったのでしょう。世の中にリメイクアニメは数多あれど、これほどまでに旧作に対するリスペクトの欠けた制作陣の言動を自分は聞いたことがありません。

ただ、劇場版ゼータは本当に黒歴史と断定できるクソ作品だったので、逆説的に考えるとオリジナル声優の方々の輝かしい経歴に汚点を残さずに済んだことは不幸中の幸いかもしれません。

劇場版ゼータが大嫌いな理由その4・何も印象に残らない主題歌

ゼータの主題歌は鮎川麻弥森口博子以外にありえません。どこの馬の骨ともしれない薄汚いチャラ男がコネで好き勝手に歌っていいものではない。恥を知りなさい。薄っぺらいクソ映画の主題歌としてはお似合いとも言えますが。

逆説的に考えるとオリジナル歌手の方々の(以下略)

劇場版ゼータが大嫌いな理由その5・CGベースのオナニー新作画

劇場版ではいくつかのシーンが新規作画されているんですが、資金の問題で基本的にTV版の旧フィルムを流用しているもんだから落差が激しくて違和感ありまくりです。また、新規シーンはCGベースで作画されているらしく、キレイだけどつまらない絵でした。ああいうのをクリエイターの自己満足と言います。より下品な言い方をするとオナニー(自慰行為)ですね*12
とはいえ、仮に潤沢な資金があって新作画で全編作り直したとしても、劇場版ゼータに対する評価は最悪のままで変わりなかったと思います。きっと「金をかけて壮大なゴミを作ったんだなあ……」という感想だけを抱いたことでしょう。アニメにおいて作画は確かに重要なファクターのひとつですが、肝心のストーリーがぐだぐだでは意味がありません。また好みの問題でもありますが、一概に最近の高精細な作画のほうが良いとは限らず、特にゼータに関しては昔のTV版の作画のほうが心象表現や迫力面(スピード感)の点でむしろ優れていると感じます。昨今のアニメは確かに昔に比べて映像技術面の向上はめざましいものがあり、中にはTVシリーズであっても昔のOVA並みの作画密度に仕上がっている作品もあるものの、お上品にまとまっているだけで、キャラクターデザインもストーリーも、どれも似たり寄ったりの薄くてつまらない作品が増えました。表面的な見た目を取り繕う小手先の技術ばかり先鋭化して、肝心の魂*13が抜け落ちています。作画コスト低減の名目でCGにばかり頼るようになったメカ表現の衰退もひどい有様です*14。日本には迫力のあるメカを描けるアニメーターはもうほとんどいないんでしょう*15*16*17。面白かったと言えるロボアニメはダイミダラーが最後でした。

ちなみに劇場版ファーストでは安彦良和によって作画がいくつか手直しされていますが、TV版放送終了からほとんど時間をおいていないので違和感がないし、そもそも基本的なストーリーや結末自体はTV版から変わっておらず、重要なシーンもほとんどカットされていません。同じ「劇場版」でもえらい違いです。劇場版ファーストは公開から40年近く経つ今でもなおガンダム入門としてお勧めできる金字塔*18ですが、クソ劇場版ゼータは検討の対象にすらなりません。口にするのも汚らわしい。
ところで、なぜかファーストガンダムを今の映像技術でリメイクすることを事あるごとに希望する人がいるようですが、絵がキレイなだけでコレジャナイ感満載の残念な駄作に成り下がることは目に見えています。そもそも今の時代メカを描けるアニメーターが絶滅危惧種に指定されているどころか、セイラさんもブライトさんもミライさんもマ・クベもナレーターも亡くなってしまっているのに、オリジナルを超える感動をもたらすものが作れるわけがありません。ファーストもTV版ゼータも、オナニー技術や札束によって生まれた作品などではないのです。あの時代こそが作り上げた、代替の利かない唯一無二の珠玉の作品なのです。

総論

クソ劇場版ゼータをこれから観ようとしている人にひとこと言っておきます。

観ないでください(怒)
(声:シャクティ

*1:ゼータのウェイブライダー可変機構は複雑で、2次元の嘘も含まれているため、完全変形とプロポーションを両立するガンプラは当時の技術では不可能だったようです。MG以降はカトキ臭が強すぎて「ゼータガンダム」らしくないので買いませんでした。現在の技術であっても完全変形を目指すとMS形態の可動や保持力が犠牲になってしまうため、設計者泣かせは相変わらずのようです。

*2:ウェイブライダー百式を助けて大気圏突入するシーンも印象的でした。続編のダブルゼータでもジュドーのゼータがエルピー・プルキュベレイMk-IIを救う形で大気圏突入するシーンがあり、演出上の要となっています。

*3:ゼータではファーストの主人公アムロ・レイと宿敵シャア(クワトロ・バジーナ)が共闘し、「永遠のフォウ」では二人がフォウの死に立ち会っていることも見逃せません。ゼータではアムロは宇宙には上がらず、地球でエゥーゴに協力する組織「カラバ」の一員として戦います。放送当時はアムロガンダムに乗らないことが批判されていたようですが、ゼータの主人公はカミーユとシャアであり、また結果的に逆襲のシャアでのニューガンダムの活躍の印象を強めることになったので、むしろゼータでは脇役に徹していてよかったのではないかと思います。

*4:各章のサブタイトルのダサさも富野のセンスの終焉を物語っています。寒すぎて吐き気がするうえにじんましんが出ます。

*5:Vガンダムに代表される「黒富野」に対して、いわゆる「白富野」が嫌いというわけではなく、例えばブレンパワードは好きな作品のひとつです。ターンエーもガンダムとして観ると微妙ですが、悪くはなかったと思います。

*6:カミーユの精神崩壊は酸素欠乏症が原因とかいう推測をする人もいますが、のちにダブルゼータのラストで回復しているらしき描写があることから、単なる酸素欠乏症ではないと思います。

*7:ちなみに小説版のゼータのラストでは、カミーユパイロットスーツのバイザーが宇宙空間で開放された状態が続いている描写があり、あれは酸欠どころか死んでいるんじゃないんでしょうか。

*8:F91もラストにシーブックとセシリーが宇宙空間で抱き合って終わりですが、F91のほうが遥かに爽やかです。

*9:ダブルゼータは最後まで食わず嫌いせずに観て欲しい作品です。たまにダブルゼータ黒歴史扱いする連中がいますが、そのような節穴の戯言は放っておきましょう。確かに前半の作風はおふざけが過ぎる部分もあり、ゼータから続投しているキャラクター達の印象まで変わってしまっていることに憤る気持ちも理解できますが、総括すれば間違いなく名作のひとつであり、クソ劇場版ゼータなど比べ物になりません。

*10:そもそもクソ劇場版は「ゼータガンダム」ではなく、ただのパチモン・贋作・偽物に過ぎないのですから、これからは「ゼットガンダム」いやむしろ「ゼットガソダム」と呼んで区別するべきでしょう。シティーハンターに対するエンジェルハートのように、公式による低俗な同人駄作品だと考えたほうがよいです。

*11:ブレンパワードの伊佐未勇(白鳥哲)、宇都宮比瑪(村田秋乃)、カナン・ギモス(朴璐美)など、舞台出身の新人(当時)でも、役に合った声質や技量のあるオリジナルキャスティングであれば当然問題ないのですが、クソ劇場版では周りが全員本職の声優の中、池脇氏だけが棒演技のド素人で完全に浮いていました。コネか枕か知りませんが、映画の吹替にヘタクソな芸人を使うのとまったく同じヤリ口で吐き気がします。

*12:ガンダムUCにも言えるんですが、どうでもいいマイナーMSやハリボテのネオジオングといったクソどもを画面に出すヒマがあったら、もっと原作通り登場人物を掘り下げて欲しかったです。主人公バナージ・リンクスの人物像は監督の趣味趣向で捻じ曲げられた挙句、重要なシーンはカットされ、完全にオナニー販促アニメに成り下がってしまいました。

*13:抽象的な表現ですが、そもそも animate とは「生命を吹き込む」という意味を持っています。単に動画枚数を増やして絵を動かせばいいというものではありません。

*14:ダブルゼータのドッキングシーンに代表されるように、80年代アニメでは今のCGアニメーターが束になっても到底かなわない本気のメカ作画を要所要所で観ることができます。今のアニメがどれだけ高精細になろうとも、どれだけCGが進歩しようとも、逆襲のシャア0080の作品全体に流れる覇気には遠く及ばないでしょう。

*15:2000年代以降は人物の作画(特に表情)もあっさりしすぎていて迫力というか覇気のない作品が多く、TV版のゼータやダブルゼータといった1980年代アニメの鬼気迫る表情に比べると平坦・平凡そのものです。線画も勢いがなく単調・無機質で、とにかく画(え)に生気が感じられない。キャラクターデザインも80年代のほうがメリハリがあって、男性は凛々しく、女性は可憐でありながらも、どこか親しみが持てます。

*16:そういえばガンダムシリーズのVHSやLDのパッケージイラストは素晴らしいものが多いんですが、なぜかDVDのパッケージは総じてひどいですね。ガンダムに限らず、2000年代以降、絵柄が残念な方向に変化してしまったアニメーターが多い。誰とは言いませんが。

*17:日本ではCGで人物やロボットをクルクル回してアニメーターがドヤ顔をするのが流行っているようですが、できて当然のものを見せられても何の感慨もわきません。逆に痛々しいです。板ポリゴンに魔法陣などの紋様を描いたテクスチャを張ってクルクル回すのも流行っていますが、これは随分前からゲームでお馴染みとなっているビルボード手法の一種で、リアルタイムであればともかくプロダクションレンダリングでやるとすさまじい手抜き感が漂う安っぽいエフェクトです。

*18:ただし音声再録の特別版、テメーはダメだ。余計なことして名作を汚しまくった大罪は万死に値する。コイツはクソ劇場版ゼータと並ぶ黒歴史のひとつなので良い子のガンダマーは観てはいけません。ドラゴンボール改悪も同様ですが、2000年代以降の日本のアニメは本当に過去の遺産に平気で泥を塗る行為ばかりが目立っています。もうジャパニメーションはオワコンですね。

喫煙者は肺ガンで苦しみながら死んでくれ

最近窓を開けると、決まった時間にタバコの悪臭が漂ってくるようになりました。どうも新しくやってきた近隣の住民が吸っているようです。窓を閉め切っていれば悪臭が入ってくることはないのですが、自分はエアコンが嫌いで、夏場は換気扇と扇風機だけでしのいでいるので、タバコ臭はかなりウンザリします。夜中の12時を過ぎても漂ってきます。

タバコを吸う人間は全員肺ガンで例外なく苦しみながら後悔とともに死んでくれ。ちなみに痩せ型の人間は、肥満体型の人間よりも喫煙による健康リスクが高いそうです。同程度にリスクがあればいいのに。チッ。非喫煙者で痩せ型の人は特に受動喫煙副流煙に注意しましょう。最近よく見かける電子タバコや加熱式タバコは、目に見える煙は出なくても危険性に変わりはありません。とにかく喫煙者には近寄らないのが無難です。

自分の父親も恥ずかしいことに喫煙者ですが、ヤツは昔自家用車内で子供の前でも堂々と吸っていました。妻子の健康よりも、自分のヤニ切れ解消のほうが大事だったようです。運転席の窓を開けても腐った空気がかき混ぜられるだけで意味ねーよ。逆に臭いんだよ。エアフロー考えろよ。庭にタバコの吸い殻が捨てられているのを見て、タバコのフィルターは自然分解されにくいからポイ捨てするなとヤツに注意したこともありますが、結局聞く耳持たずでした。最低ですね。
弟も喫煙者で、残念ながらクソ親父の悪影響を受けてしまったようです。幼い頃は同じ環境で育った兄弟なのに、なぜタバコに対する考え方がこうも違ってしまったのか。まあそれ以外の点でもかなり性格や主義思想が違うんですけどね。

歩きタバコやポイ捨ては逮捕してくれ

歩きタバコ(路上喫煙)とポイ捨ても不快です。喫煙者の後ろを歩くと臭くてしょうがないのでかなり距離を空けます。吸い殻を目の前でポイ捨てされる場面に遭遇することはめったにないのですが、道端や側溝にまだ火のついたタバコが捨てられてあるのを見ると激しい殺意を覚えます。道路のど真ん中に転がっていることさえあります。あと歩きタバコは子供の目線の高さに火元がプラプラすることになるので、恐ろしい事故につながる可能性があります。できれば身をもってその危険性を教えてやるために、歩きタバコする喫煙者の手からタバコを取り上げてそいつの目玉に押し付けてやりたいですね。ジュッと。

路上喫煙やポイ捨てを取り締まる条例もありますが、地域によって違うとか、せいぜい罰金とか、日本は喫煙者に甘すぎです。迷惑どころか危険行為なので現行犯逮捕するべき。

飲食店はすべて完全禁煙にしてくれ

自分が今住んでいる地域では、日高屋とかいうやる気のない分煙(笑)をしているつもりのアホなチェーン店は除き、喫煙可能な飲食店はほとんど見かけず、かつて分煙だった飲食店も完全禁煙に移行したので喜ばしい限りです。サイゼリヤすかいらーくグループといった大手も今年から全店舗で完全禁煙になりました。ですが、今の職場のある新宿はひどいですね。チェーン店はたいてい禁煙か分煙ですが、ほとんどの喫茶店は普通にタバコ吸えます。時代遅れもいいところです。分煙すらされていないところは二度と行かないことにしています。臭くて何も楽しめない。分煙であっても喫煙席の近くは基本臭いです。だいたいタバコ吸って麻痺した連中の汚い舌や委縮した脳に何の味が分かるっていうんでしょうか。喫煙者は味覚・嗅覚・知能障害者です。

普段の飲食店は自分の意志で選べるので回避することもできるのですが、問題は飲み会ですね。隣が喫煙者だった日は地獄です。隣ではなくても部屋に喫煙者がいただけで、服が臭くなるのが不快です。健康被害を最小限に抑えるためにも、基本的に無意味な二次会などには参加せず、終わったらさっさと帰ります。

喫煙所は密閉・隔離してくれ

屋外の喫煙所には、簡易的な仕切りがあるだけで、エアフィルターを設けずに開放されているものがあります。ふざけんなと。福島原発放射性物質で汚染された地下水を薄めて海洋に放出しようとしたり、除染で発生した汚染土をこっそり日本中にばらまこうとしたりする日本政府というか安倍政権と同類の犯罪行為ですね。空も海も大地も、地球上のみんなのものです。共有財産を勝手に汚染するな。
そういえばこの前ヨーロッパに出張に行ったとき、地方の駅ではプラットフォームに喫煙所というか喫煙エリアがありました。しかし看板があるだけで、フィルターどころか仕切りすらありません。かなり残念な光景でした。ていうかプラットフォームは完全禁煙にしろよ。

タバコは絶滅してくれ

喫煙者全員がマナーやモラルの低い人間だとは思いませんが、「百害あって一利なし」であることがすでに科学的に証明されているタバコを、自らの意志でやめることができないのは、ただの依存症です。タバコは趣味嗜好などではなく、アルコール中毒麻薬中毒と同じく病気です。病人は病院に行ってください。喫煙者がガンで苦しんで死のうが知ったこっちゃないんですが、奴らのせいでガン患者が増えて医療費として税金が無駄に使われていると思うと反吐が出ます。喫煙者のガンは治療する必要などありません。

喫煙者の人口割合が多かった時代に作られた昔の小説や映画、漫画やアニメなどでは、タバコを演出上の小道具のひとつとして重宝していたことがありました。とはいえ、文学や創作の世界にまで目くじらを立てるつもりはありません。タバコを吸っているけど個人的に気に入っている架空の登場人物、というのは自分にもあります*1*2。むしろ、かつてはそういう時代もあったんだね、ということを振り返ることができるという意味では貴重なので、昔の時代を描く場合はヘタに自重してタバコ演出を削除する必要はないと思います。

だが現実世界ではタバコは絶滅せよ。あとJTとかいう悪魔の手先は滅びろ。

*1:ブラック・ジャックとか、『恋は雨上がりのように』の店長とか、『野々村病院の人々』の海原琢麻呂とか。

*2:シティーハンターの主人公・冴羽リョウは、原作漫画ではかなり頻繁にタバコを吸っているシーンが出てきますが、アニメでは彼の喫煙シーンは滅多に出てきません。1, 2, 3では少なくとも自発的に吸うシーンは一切ありません。おそらく子供への影響を考慮してのことだと思いますが、そのこともあってアニメ版のリョウのほうが個人的に好感度が高いです。

東池袋自動車暴走死傷事故 厳罰化要望に署名しました

ameblo.jp

自分も微力ながら郵送にて署名に協力させていただきました。

署名用紙 (PDF) は以下からダウンロードできます。署名活動は8月末までを予定されているそうです。
このブログをご覧の方々、よろしければぜひ。

加害者の飯塚幸三は反省も謝罪もせず、それどころか権力で殺人を揉み消そうとしています。
そのような蛮行、決して許されるべきものではありません。

enumの読み方・発音はイニューム

ほとんどのプログラミング言語には列挙型 (enumeration type) あるいは列挙体 (enumeration) というデータ型があり、有限集合を順序付けて定義するのに使用します。C系の言語ではキーワードenumを使用します。

/* 軍艦の種類を表す列挙型 */
typedef enum MyWarshipType {
    MyWarshipType_BattleShip, /* 戦艦 */
    MyWarshipType_AircraftCarrier, /* 空母 */
    MyWarshipType_LightAircraftCarrier, /* 軽空母 */
    MyWarshipType_HeavyCruiser, /* 重巡洋艦 */
    MyWarshipType_LightCruiser, /* 軽巡洋艦 */
    MyWarshipType_Destroyer, /* 駆逐艦 */
    MyWarshipType_Submarine, /* 潜水艦 */
    /* C99 や C++11 では末尾要素の後のコンマが許可される */
} MyWarshipType;
/* 列挙型変数の例 */
MyWarshipType type = MyWarshipType_HeavyCruiser;

列挙型の各メンバーは列挙子 (enumerator) と呼ばれます。C/C++では各メンバーに整数値が割り当てられ、ユーザーコードで明示的に指定しなければ最初のメンバーの内部数値はゼロとなり、以降のメンバーは 1, 2, 3, ... のように自動的にインクリメントされた連番が割り振られます。

Javaには当初列挙型がありませんでしたが、有用性を認めたのか、Java 1.5で結局導入されました。ただしJavaの列挙型は参照型つまりclassの一種であり、そのままでは整数型と相互に変換することはできません。

C#には当然最初から列挙型があります。C#の列挙型はC/C++と相互運用しやすい値型となっていて、キャストだけで整数型と相互に変換することもできます。

C++ではC言語互換のenumをサポートするほか、C++11ではスコープを持つ列挙型 (scoped enum) が導入され、JavaC#に近い記述ができるようになりました*1

enumの発音

ところで、このenumをどう発音しますか?

もともとenumは、前述のように英語の enumeration から来ています。米国式発音では「inùːməréiʃən」、英国式発音では「injùːməréiʃən」です。もしカナで無理やり表記するとすれば、それぞれ「イヌーメレイション」「イニューメレイション」といったところでしょうか。アクセント(第1強勢)の位置は-tionの前、第4音節にあります。
また、enumerator の発音は「inúːmərèitər」もしくは「injúːmərèitər」であり、カナ表記するならば「イヌーメレイター」「イニューメレイター」といったところです。アクセントの位置は第2音節にあります。
動詞形 enumerate の発音は「inúːmərèit」もしくは「injúːmərèit」です。カナ表記するならば「イヌーメレイト」「イニューメレイト」といったところです。

したがって、enumは「inúːm」もしくは「injúːm」と発音するのが正しいと思います。カナ表記するならば「イヌーム」もしくは「イニューム」です。個人的には「イニューム」推し*2ですが、米語圏エンジニア相手の会話であれば「イヌーム」と発音するほうが伝わりやすいかもしれません。
ちなみに.NETにはIEnumeratorインターフェイスおよびIEnumerableインターフェイスが存在しますが、自分はそれぞれ「アイ・イニューメレイター」「アイ・イニューメラブル」と発音しています。
また、Windows APIにはトップレベルウィンドウを列挙する関数EnumWindows()が存在しますが、自分は「イニューム・ウィンドウズ」と発音しています。

たまに見聞きするのが、「イナム」とか「イーナム」とかいう発音ですが、これは正直ダサいと思います。略す前の原語を知らずに無理やり字面だけを読んでいるようで、個人的に言わせてもらうと教養を感じさせない、悪く言うと幼稚な印象さえ受けます。極めつきは「エナム」。英語が苦手な日本人らしい、ダサい発音の極致です。
ネイティブでもenumの発音に関しては諸説あり、「イナム」はともかく「イーナム」のほうはネイティブも使っている人がいるようで、誤りとまでは言えないようですが、個人的には嫌いな発音です。

というわけでenumイニューム。異論は認め……ない。どうしても「イーナム」と発音したい人は、せめてenumの語源と、enumeration, enumerator, enumerateの正しい発音くらいは把握したうえでどうぞお好きなように。

余談:charの発音

文字型charの発音に関しても意見が分かれるかもしれません。自分は語源の character そのまま、すなわち「kǽrəktər」=「キャラクター」と発音しています。Bjarne Stroustrupを始め、ネイティブでも字面だけを読んで「tʃɑ́ːr」=「チャー」とかいう人もいますが、これもまたダサいです。日本では「キャラ」と略す人もいるようですが、これは英語圏・米語圏では通用しないと思います。
ちなみにwchar_tは「ダブリューキャラクター・アンダーティー」もしくは「ワイドキャラクター・アンダーティー」と発音しています。

*1:このscoped enumは、もともとマイクロソフトが開発した.NET用マネージ言語C++/CLIにおいて、マネージ列挙型のために拡張された構文がベースになっています。

*2:自分の学部生の頃の恩師である助教授(当時)が「イニューム」と発音されていたので、その影響も少なからずあります。

ログ用に時刻を取得して文字列化する (C#, Java, POSIX/Win32)

アプリケーションのデバッグや、デプロイ後の問題解析に最も重要な役割を果たすのはログです。ログの機能や精度・粒度によって、問題解決のしやすさがほぼすべて決まります。また、ログは単なるトラブルシューティングだけでなく、場合によってはユーザー操作記録などの簡易的なエビデンス(証拠)としても使えます*1
通例、動作ログにタイミング情報を記録する際、レコード行に時刻情報を含めることで、状態遷移の正しさの確認や速度性能解析に役立てることができます。いわゆるprintfデバッグです。レコードの時刻情報はログをファイルに記録するときだけでなく、デバッグコンソールに出力するときにも役立ちます。

また、秒単位ではなくミリ秒単位の時刻情報が記録できると性能解析に便利です*2。なお、時刻は協定世界時 (UTC) で記録しておくと逆シリアライズが簡単になるというメリットがあるのですが、現地時刻(ローカル時刻)とタイムゾーン情報(GMTからのオフセット)を記録する方式のほうが、人間が直接ログレコードを見て判断しやすくなるというメリットがあります。

ログファイルのフォーマットはパフォーマンスの観点から、ファイルの全書き換えではなくプレーンテキストの末尾追加で済むCSVフォーマットやTSVフォーマットなどが好ましいです*3。また、後で回収できるように、日付ごとにファイルを分けて作成したり、直近数か月分を保持して古いものは自動削除する循環ログにしたり、といった機能を実装します。自分はロギング機能を自前で実装したことが何度かありますが、通例 log4jlog4net を使っているプロジェクトも多いのではないかと思います。

今回はロギングの実装までは踏み込まず、C#, Java, C/C++におけるそれぞれの時刻情報取得と文字列化(フォーマット)についてのみ説明します。
ちなみにISO 8601は日付と時刻の間にTを入れるルールになっているのですが、今回は見やすさ優先のためASCII空白0x20にしました。

C#

C# (.NET) は後発なだけあって一番簡単です。洗練されていて美しいですね。余計な解説をするようなことはほとんどありません。

using System;

public class DateTimeFormatTest {
    public static void Main() {
        const string format = "yyyy-MM-dd HH:mm:ss.fffzzz";
        // GMT からのオフセット値が 0 の場合、タイムゾーンは "+00:00" になる。 
        Console.WriteLine("Local = " + DateTime.Now.ToString(format));
        Console.WriteLine("UTC   = " + DateTime.UtcNow.ToString(format));
    }
}

もしローカル時刻とUTC時刻との間の変換が必要な場合、TimeZoneInfo.ConvertTimeFromUtc()TimeZoneInfo.ConvertTimeToUtc()を使います。

なお、ラウンドトリップ書式指定子 "O", "o" を用いることで、簡単にISO 8601形式の書式化(シリアライズ)と解析(逆シリアライズ)ができるようです。

Java

Javaも時刻関連ライブラリは最初期から実装・標準化されています。しかしタイムゾーンの扱いが若干厄介な印象を受けます。ログのパーサーを書くときに考慮する必要があります。
注意点として、SimpleDateFormatはスレッドセーフではないので、スレッド間でインスタンスを使いまわすのは厳禁です。スレッドごとにインスタンスを作成する必要があります。

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

public class Main {
    private static final String FORMAT = "yyyy-MM-dd HH:mm:ss.SSSXXX";
    public static void main(String[] args) {
        // GMT からのオフセット値が 0 の場合、タイムゾーンは "Z" になる。 
        System.out.println("Local = " + (new SimpleDateFormat(FORMAT, Locale.ROOT).format(new Date())));
        final SimpleDateFormat sdf = new SimpleDateFormat(FORMAT, Locale.ROOT);
        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
        System.out.println("UTC   = " + sdf.format(new Date()));
    }
}

C/C++

.NETもJavaもプラットフォーム非依存の高水準環境であり、標準ライブラリも充実しているので特に困るようなことはありませんが、問題はC/C++です。こいつらは標準化がとにかく遅い言語で、時刻関連の標準ライブラリは貧弱の一言です。まともな情報を取得するには、プラットフォームごとのライブラリやAPIを使わざるを得ません。

POSIX

UNIX時刻を表すtime_t型からtm構造体*4への変換がミソです。ちなみに逆変換をする関数mktime(), timegm()も存在します。

なお、標準Cライブラリのlocaltime(), gmtime()はスレッドセーフでないので使わないようにします。代わりにリエントラントなlocaltime_r(), gmtime_r()を使います。

ところでUNIX系OSの場合、通例time_t型は32bit OSでは32bit整数、64bit OSでは64bit整数で実装されていることが多いため、32bit UNIX2038年問題を回避できません。2038年は32bit UNIXが完全に死ぬ年です。サーバーはもちろんPCやモバイルでもすでに32bit UNIXは退役していますが、組み込み系はヤバそうですね。かつての2000年問題並みにヤバいのでは? だからPOSIXはクソなのだ。

ミリ秒単位あるいはそれよりも高分解能で時刻を得たい場合は、gettimeofday()timevalの値を取得するか、またはclock_gettime(CLOCK_REALTIME)timespecの値を取得します。ただしgettimeofday()のほうは廃止予定 (obsolescent) になっているので、少なくとも新しいコードでは使わないほうがいいです。LinuxFreeBSDでは第2引数にtimezone構造体へのポインタを指定できますが、Linuxではこの構造体も廃止予定 (obsolete) になっています。

CLOCK_REALTIMEはシステム時刻の変更や、NTPによる同期の影響を受けるため、いわゆるカレンダー時刻を取得するのに使えますが、基本的に2点間の経過時間計測には使えません。2点間の経過時間計測にはCLOCK_MONOTONICCLOCK_MONOTONIC_RAWを使うべきです。また、マイクロ秒単位やナノ秒単位での記録が不要な場合、かつLinux環境では、CLOCK_REALTIME_COARSECLOCK_MONOTONIC_COARSEを使うとパフォーマンスが向上するそうです。

下記コード例で使用しているtm構造体のメンバーのうち、タイムゾーン関連のtm_gmtofftm_zoneはC標準ではなく、BSD/GNU拡張になっています。そのため環境によっては使えない可能性もあります。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//#include <sys/time.h>

int main() {
    printf("sizeof(time_t) = %d\n", static_cast<int>(sizeof(time_t)));
#if 0
    // 秒単位まででよければ、C 標準の time() 関数を使う。
    const time_t now = time(nullptr);
#elif 0
    // C 標準では time_t の実際の型が整数であるかどうかは規定されていない。
    // POSIX では符号付き整数であることが保証される。
    time_t now = 0;
    if (time(&now) == -1) {
        puts("Failed to get time!!");
        return -1;
    }
#elif 0
    // gettimeofday() を使うと、マイクロ秒単位で時刻を求めることができる。
    // timezone 構造体は廃止予定の非推奨機能。
    // gettimeofday() 関数は廃止予定の非推奨機能。
    struct timeval tvo = {};
    //struct timezone tzo = {};
    //gettimeofday(&tvo, &tzo);
    gettimeofday(&tvo, nullptr);
    const time_t now = tvo.tv_sec;
#else
    // clock_gettime() を使うと、ナノ秒単位で時刻を求めることができる。
    struct timespec tso = {};
    if (clock_gettime(CLOCK_REALTIME, &tso) != 0) {
        puts("Failed to get clock time!!");
        return -1;
    }
    const time_t now = tso.tv_sec;
#endif

    // ctime_r() や asctime_r() は出力フォーマットがお粗末なので却下。
    struct tm tmo = {};
    if (localtime_r(&now, &tmo) != nullptr) {
        // tm_gmtoff, tm_zone は BSD/GNU 拡張。
        // tm_gmtoff は GMT (UTC) からのオフセットで、秒単位。
        const int gmtOffsetHourPart = tmo.tm_gmtoff / 3600;
        const int gmtOffsetMinPart = abs(tmo.tm_gmtoff / 60) % 60;
        // ミリ秒部分を浮動小数点数で計算して単純に "%03.0f" 書式で四捨五入すると、例えば 999.9 の繰り上がりで破たんする。
        //const int msPart = static_cast<int>(tvo.tv_usec / 1000);
        const int msPart = static_cast<int>(tso.tv_nsec / 1000L / 1000L);
        printf("Local = %04d-%02d-%02d %02d:%02d:%02d.%03d%+03d:%02d\n",
            tmo.tm_year + 1900, tmo.tm_mon + 1, tmo.tm_mday, tmo.tm_hour, tmo.tm_min, tmo.tm_sec,
            msPart,
            gmtOffsetHourPart, gmtOffsetMinPart);
        // DST = Daylight Saving Time: 夏時間(サマータイム)。
        printf("WeekDay = %d, YearDay = %d, DST = %d, Zone = \"%s\"\n",
            tmo.tm_wday, tmo.tm_yday, tmo.tm_isdst, tmo.tm_zone);
        // +hhmm や -hhmm 形式でよければ strftime() を使うこともできる。
        char buf[128] = {};
        strftime(buf, sizeof(buf), "%F %T %z (%Z)", &tmo);
        printf("Local = %s\n", buf);
    }

    if (gmtime_r(&now, &tmo) != nullptr) {
        printf("UTC   = %04d-%02d-%02d %02d:%02d:%02d\n",
            tmo.tm_year + 1900, tmo.tm_mon + 1, tmo.tm_mday, tmo.tm_hour, tmo.tm_min, tmo.tm_sec);
    }
}

Win32

Visual C++のランタイムライブラリには、POSIXlocaltime_r(), gmtime_r()に相当するlocaltime_s(), gmtime_s()がMSVC 8.0以降のSecure CRTとして実装されていますが、引数や戻り値の仕様が異なるので注意が必要です。また、C11規格(C++11ではない)ではlocaltime_s(), gmtime_s()が追加されていますが、MSVCの同名関数とはインターフェイスが異なるので注意が必要です。ちなみに、MSVC 8.0以降のtime_tは64bit化されていて、32bitプラットフォームでも既定で64bit整数となり、2038年問題を回避できるようになっています*5

いずれにせよ、Windowsにはgettimeofday()clock_gettime()が実装されておらず、tm構造体のtm_gmtoff, tm_zoneメンバーも実装されていないので、ミリ秒単位で時刻を求めたり、タイムゾーン情報を取得したりする場合はWindows APIを直接叩くのがベストだと思います。

なお、TIME_ZONE_INFORMATION構造体のメンバーのうち、夏時間の扱いはよく分かりませんでした。日本はサマータイムとかいうクソシステムなんぞ採用していないのに、DaylightBias-60となります。
このメンバーはGetTimeZoneInformation()関数の戻り値がTIME_ZONE_ID_DAYLIGHTであるときのみ使われるということでしょうか。

This value is added to the value of the Bias member to form the bias used during daylight saving time. In most time zones, the value of this member is –60.

標準Cライブラリが第一級市民扱いとなっているUNIXと違い、Windowsでは第一級市民ではなく、標準C/C++ライブラリはすべてWin32 API上に構築されています。Win32では最初から時刻表現が64bit化されているので、32bitアプリケーションでも2038年問題を回避できる仕様になっています*6

#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
#include <Windows.h>

int main() {
    setlocale(LC_ALL, "");

    SYSTEMTIME loc = {};
    ::GetLocalTime(&loc);
    SYSTEMTIME utc = {};
    ::GetSystemTime(&utc);

    TIME_ZONE_INFORMATION tzInfo = {};
    const DWORD timeZoneId = ::GetTimeZoneInformation(&tzInfo);
    printf("TimeZoneId = %lu\n", timeZoneId);
    if (timeZoneId != TIME_ZONE_ID_INVALID) {
        // TIME_ZONE_INFORMATION::Bias は分単位。
        const int gmtOffsetHourPart = -tzInfo.Bias / 60;
        const int gmtOffsetMinPart = abs(tzInfo.Bias) % 60;
        printf("Local = %04hd-%02hd-%02hd %02hd:%02hd:%02hd.%03hd%+03d:%02d\n",
            loc.wYear, loc.wMonth, loc.wDay, loc.wHour, loc.wMinute, loc.wSecond,
            loc.wMilliseconds,
            gmtOffsetHourPart, gmtOffsetMinPart);
        printf("WeekDay = %hd, Zone = \"%S\"\n", loc.wDayOfWeek, tzInfo.StandardName);
    }
    printf("UTC   = %04hd-%02hd-%02hd %02hd:%02hd:%02hd.%03hd\n",
        utc.wYear, utc.wMonth, utc.wDay, utc.wHour, utc.wMinute, utc.wSecond,
        utc.wMilliseconds);
}

説明の簡略化のためprintf系を使いましたが、C/C++で文字列バッファにフォーマット出力するときはsnprintf系やstd::stringstreamを使います。Boost.Formatを使うのもひとつの手です。もしWindows限定でよければATL::CString::Format()を使うと簡単です。

Boost C++ライブラリのboost::posix_timeboost::date_timeを使えば、一応プラットフォーム非依存なコードを書けそうですが、JavaC#のように言語標準の方法で手軽かつ簡単に、というわけにはいきません。

ところで、C++11では時刻を扱うstd::chronoが追加されましたが、このライブラリは設計が大仰なわりに機能が貧弱で、しかも使い方が直感的でなく、かなり面倒です。C++20で日付フォーマット関連の機能が追加されるらしいのですが、今更ですね。標準化が遅すぎる。C/C++JavaC#に比べて言語機能もライブラリも10年どころか20年以上遅れをとっています。先発言語の事例を参考にして十分に練り込み、利便性の高いものを出してくるのであればそれでも我慢できるかもしれませんが、標準化委員会の連中は時代に逆行したユーザビリティ最悪のオナニーツールしか作れないようです。生産性や移植性の低いC/C++は、21世紀における新時代のアプリケーション開発に使うべき言語ではありません。引退すべき言語とまでは言いませんが、今後はハードウェア層に近いバックエンドの記述などにとどめるべきです。

*1:もちろんコンプライアンス上の問題がない限り、という条件は付きます。ログにユーザーIDやパスワードを平文で記録するようなことは絶対にしてはいけません。最近の事例だとFacebookがやらかしていたことが報じられています。Facebookが数億件分のパスワードを暗号化しないままサーバーに保存していたことが明らかに - GIGAZINE

*2:システム時刻を処理時間計測に使うと、アプリケーション稼働中にシステム時刻が変更された場合に破たんするので、単調増加することが保証されるmonotonic (steady) なタイマーを使って2点間の差を計算するのがセオリーです。例えばJavaの場合はSystem.currentTimeMillis()ではなくSystem.nanoTime()の差を使います。とはいえ、稼働中にシステム時刻の変更がないという前提であれば、システム時刻を処理時間計測に使うことはできます。

*3:XMLJSONは階層構造やオブジェクトリストの表現には向いていますが、ファイルの先頭と末尾にタグやブレースの開始/終了ペアが必要であり、追記モードでファイルを開いて新しいレコードだけを後から追加するようなことができません。

*4:名前が完全に意味不明ですね。Cのライブラリは歴史的にひどい略語のシンボルが多く、可読性が最悪です。

*5:逆に言うと、古いコンパイラコンパイル&ビルドされたアプリケーションは2038年問題を回避できません。

*6:よくWindowsPOSIXに準拠していないことを批判されるのですが、そういう連中はPOSIX仕様のお粗末さを理解していません。まともにアプリケーション開発をしたことがないんでしょう。POSIXは全体的に時代遅れの遺物です。

Android Javaのアサーション

昨年2018年は転職したこともあって、一番よく使った言語はJavaでした。とはいっても、ほとんどはAndroidJavaであり、サーバーサイドやデスクトップで使われている正式なJavaとはいささか性質が異なります。

JavaC/C++C#と違ってプリプロセッサをサポートしないのですが、そのせいで一番困るもののうちのひとつはアサーション(表明)です。アサーションは「契約による設計」(design by contract) を実現・実践するために有効なプログラミング手法であり、呼び出し元が保証すべき事前条件や、呼び出し先が保証すべき事後条件をコード中に記述し、条件が満たされなかった場合はプログラムを続行せずに中断します。一方、リリースビルド(本番環境)でアサーションが無効になっているときは、引数として渡した式(表明したい条件)が評価されずに実行コストゼロとなることが好ましく、そのためC/C++C#ではコンパイルオプションに応じて有効/無効が切り替わるプリプロセッサマクロやConditional属性付きメソッドによって、ゼロオーバーヘッドのアサーションを実現しています。逆に言うと、リリースビルドではアサーションの条件式は評価されないため、必ず実行されないと困るような式(システムに対して副作用のある式)は記述しないようにします。

#include <cassert>
// NDEBUG シンボルが定義されていない場合、assert マクロが有効になる。
// アサーションが有効な場合は assert マクロが実行される。無効な場合は引数の評価ごと無視される。
// 条件が満たされず、アサーションが失敗したとき、abort 関数が実行される。
static void test(const int ary[], size_t length) {
    assert(ary != nullptr);
    for (size_t i = 0; i < length; ++i) {
        printf("%d\n", ary[i]);
    }
}

C/C++標準のassert()マクロは、表明が失敗 (assertion failure) するとabort()関数でプログラムを強制終了しますが、C# (.NET) のDebug.Assert()メソッドは表明が失敗した場合に無視してプログラムを続行することもできます。MSVCのCRTマクロ、_ASSERT()_ASSERTE()に近い挙動です。

// DEBUG シンボルが定義されている場合、Assert メソッドが有効になる。
// アサーションが有効な場合は Assert メソッドが実行される。無効な場合は引数の評価ごと無視される。
// 条件が満たされず、アサーションが失敗したとき、エラーメッセージが表示される。
static void Test(int[] ary)
{
    System.Diagnostics.Debug.Assert(ary != null);
    foreach (int x in ary)
    {
        System.Console.WriteLine(x);
    }
}    

Javaにはバージョン1.4で assert ステートメントが追加されたのですが、Javaアサーションコンパイルオプションではなく実行時オプションで有効/無効を切り替えます。JITコンパイラによる最適化がうまく機能すれば、無効時にはゼロコストとなることが期待できます。

// アサーションが有効な場合は assert ステートメントが実行される。無効な場合は引数の評価ごと無視される。
// 条件が満たされず、アサーションが失敗したとき、java.lang.AssertionError がスローされる。
static void test(int[] ary) {
    assert ary != null;
    for (final int x : ary) {
        System.out.printf("%d\n", x);
    }
}

上記の例に限れば、仮にリリースビルドでnullが渡されたとしても、途中でjava.lang.NullPointerExceptionがスローされることでプログラムは中断されますが、nullを有効値として許可しないメソッドであるということをコード上で明らかにするためにもアサーションは有用です。Java 7以降であれば、java.util.Objects.requireNonNull()を併用するとなお良いです。ちなみにJSR 305の@Nonnullアノテーション (javax.annotation.Nonnull) はメソッドインターフェイスのドキュメント化に役立ちますが、すべての処理系で使えるわけではないのが残念です。

Android Javaの場合

しかし、Android 4.xまでの仮想マシン Dalvik では、アサーションの有効/無効を切り替えるシステム プロパティの debug.assert は信頼性がなく(無視されることがある)、さらにAndroid 5.0以降の仮想マシン ART (Android Runtime) では未実装とのこと。
Android StudioJava言語組み込みのassert文を使おうとすると、以下のような静的チェックツール (lint) による警告が出ます。

Assertions are not checked at runtime. There are ways to request that they be used by Dalvik (`adb shell setprop debug.assert 1`), but note that this is not implemented in ART (the newer runtime), and even in Dalvik the property is ignored in many places and can not be relied upon.
Instead, perform conditional checking inside `if (BuildConfig.DEBUG) { }` blocks. That constant is a static final boolean which is true in debug builds and false in release builds, and the Java compiler completely removes all code inside the if-body from the app.
For example, you can replace `assert speed > 0` with `if (BuildConfig.DEBUG && !(speed > 0)) { throw new AssertionError() }`.
(Note: This lint check does not flag assertions purely asserting nullness or non-nullness; these are typically more intended for tools usage than runtime checks.)
https://code.google.com/p/android/issues/detail?id=65183

つまり現在のAndroidでは、Java言語組み込みのassert文はまともに使用できません。
代替として、上記 lint では java.lang.AssertionError を明示的にスローするコードを書くことが提案されていますが、正直そんなまどろっこしいことやってられません*1

Android開発環境が自動生成するクラスBuildConfig*2のboolean定数フィールドBuildConfig.DEBUGを使って分岐すれば、確かにコンパイラ最適化によりリリースビルドではゼロコストとなることが期待できるのですが、分岐&例外スローを逐一書くのがダルいので、自分はJUnitライクのアサーションユーティリティクラスと静的メソッドを作って対処しています。個人的には分岐やメソッド呼び出しのオーバーヘッドよりも、分岐&例外スローを逐一書くことでノイズとメンテナンスコストが増大することのほうが懸念だからです。なお、表明したい式を直接記述してbooleanで受け取るのではなく、ラムダ式で表現して遅延評価することにより、リリースビルドでの実行時コストを削減する方法もありますが、ラムダ式を使わない場合と比べてやはりコード上のノイズが増えます。

import java.util.function.*;

public class MyAssert {
    public static final boolean DEBUG = true; // BuildConfig.DEBUG で初期化する方式でも OK。

    public static void assertTrue(boolean condition) {
        if (DEBUG && !condition) {
            throw new AssertionError();
        }
    }

    public static void assertTrue(BooleanSupplier expression) {
        if (DEBUG && !expression.getAsBoolean()) {
            throw new AssertionError();
        }
    }
}

// 利用例。
MyAssert.assertTrue(ary != null);
MyAssert.assertTrue(() -> ary != null);

AndroidJavaは本物のJavaではなくJavaモドキでしかない、としばしば揶揄されますが、こういった非互換な仕様や動作には実際かなりウンザリさせられます。Oracleがブチ切れて訴訟を起こしたくなる気持ちも理解できます。自分にとってC/C++の一番嫌いなところは、処理系依存の仕様や未定義動作があまりに多すぎることですが、本来のJavaC#はそういった曖昧な仕様が極力排除されていることが最大の魅力のうちのひとつとなっています。仮にもJavaを名乗るのであれば、そういった根本的な設計思想を曲げるようなことをすべきではありません。これは技術者の倫理 (engineering ethics) に関わる問題です。

*1:ちなみにこの警告文中に記載されている例示コードですが、文末のセミコロンが欠けており、コンパイルが通りません。

*2:ちなみにBuildConfigクラスはアプリモジュールのパッケージ(名前空間)になるので、ライブラリモジュールからは直接参照することができません。

C++でstaticクラス

C#には「staticメンバーだけを定義して、実体化(インスタンス化)は許可しない」ようなクラスを簡潔に定義する手段があります。

public static class MyHelper
{
  public static int Add(int x, int y)
  { return x + y; }
}

staticクラスは状態を管理しない純粋ヘルパー(アルゴリズムや完全定数)を定義するのに便利です。代表的な例でいうと、System.Mathクラスが挙げられます。
なお、staticクラスからはサブクラスを派生させることもできません。

ちなみにJavaでもクラスをstaticキーワードで修飾することができますが、意味合いがまったく違います*1C#のほうがキーワードに即していて直感的だとは思いますが。

C++/CLIおよびC++/CXでは以下のように書くことでC#のstaticクラス相当を記述できます。

public ref class MyHelper abstract sealed
{
public:
  static int Add(int x, int y)
  { return x + y; }
};

しかし、従来の標準C++では、staticクラスを記述する文法は直接サポートされていません*2
せいぜいできることと言えば、以下のようにデフォルトコンストラクタとデストラクタを隠ぺいして実体化を禁止するくらいしかなく、C#と比べて分かりやすさはともかく美しさに欠けます。
また、C++03以前では構文上で派生を禁止することができず、派生を禁止するにはC++11以降のfinalキーワードを使う以外ありません。

// C++03 以前。
class MyHelper
{
private:
  MyHelper() {}
  ~MyHelper() {}
public:
  static int Add(int x, int y)
  { return x + y; }
};

なお、C++11以降ではデフォルトコンストラクタとデストラクタをdelete指定することで、実体化を禁止することもできます。メンバーをdelete指定する場合はprivateではなくpublicとするのがセオリーです。

// C++11 以降。
class MyHelper final
{
public:
  MyHelper() = delete;
  ~MyHelper() = delete;
public:
  static int Add(int x, int y)
  { return x + y; }
};

MSVCにはネイティブC++でもabstractキーワードを使えるように言語拡張が施されているので、abstract sealedまたはabstract finalを指定することでstaticクラスを記述することができるのですが、Clangなどでは使えません。移植性の高いコードを書きたい場合は注意が必要です。MSVCではコンパイルオプション/permissive-を付けることで独自拡張を無効化することもできます。

2023-05-07追記:
C++17以前では言語仕様にお粗末なバグがあり、コンストラクタをdelete指定しているにもかかわらず集成体初期化による実体化ができてしまいます。前述のようにデストラクタもdelete指定しておけば、少なくとも削除はできなくなるので大抵のケースでは実質的に実体化を禁止できますが、さらにコンストラクタをexplicit指定しておけばC++17以前でも確実に実体化を禁止できます。C++20以降ではこの仕様バグが修正されています。

class MyHelper final
{
public:
  explicit MyHelper() = delete;
...
};

*1:Javaのクラスに対するstatic指定子は、入れ子になったネストクラスにのみ適用可能です。staticを指定しないネストクラスは外部クラスのインスタンスを暗黙的にキャプチャするインナークラス(内部クラス)となり、メソッド内で定義可能な匿名クラスやローカルクラスと似たようなクロージャ特性を持ちます。staticを指定したネストクラスは外部クラスのインスタンスをキャプチャしない静的クラスとなり、C#のネストクラスに近くなりますが、ネストクラスのメンバーに対するアクセス指定子の扱いがC#とは異なります。個人的にJavaのネストクラスは内部クラス/静的クラス問わず直感性に欠け、落とし穴の多い欠陥機能だと思っています。Nested Classes (The Java™ Tutorials > Learning the Java Language > Classes and Objects)

*2:C++では必ずしもクラスに関数を定義する必要はなく、クラスに属さない名前空間レベルの関数を定義することもできるので、これまでstaticクラスのサポートが重要視されてこなかったのかもしれません。