Integerの比較で結果が安定しない
というのでハマった、という話を聞いたので
なぜそうなるのかを深堀りして確認した。
Integerの比較で結果が安定しない、なぜか?
出力結果
true false
短い回答
Integerは参照型なので、==
を利用した比較では値の比較ではなく
それらが同一のインスタンスであるかどうかを判定する。
そのため、値の比較にはInteger.equalsメソッドを利用する必要がある。
長い回答
正しい比較ではないのは言語仕様からも明らかなものの
何故こうなるのかを調べるために内部での仕様を確認していく。
前提
利用したJava環境、参照したソースのバージョンはOracleJDK 13.0.1
書くまでも無いけれどInteger同士の値の比較をしたい訳なので
intで比較したときと同様の結果になってほしい。
先にint同士で比較した望ましい結果を確認する。
出力結果
true true
当然、Integerの場合でもこのように動いてほしい。
実際に行われていること
ではint同士の比較と、Integer同士の比較を行う小さなコードを書いて
それぞれのバイトコードを比較して差分を確認する。
int同士の比較
intの比較をして出力するだけのクラス gist.github.com
javap -v -classpath classes IntCompare.class
として上記のクラスを出力した結果
gist.github.com
Integer同士の比較
Integerの比較をして出力するだけのクラス gist.github.com
javap -v -classpath classes IntegerCompare.class
として上記のクラスを出力した結果
gist.github.com
差分
基本データ型のintへの代入と、参照型のIntegerへの代入部分だけを抜き出して比較する
int
Code: stack=3, locals=3, args_size=1 0: bipush 100 2: istore_1
Integer
Code: stack=3, locals=3, args_size=1 0: bipush 100 2: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 5: astore_1
bipush, isotre, astoreなどのオペコードについてはここを見てもらうとして
Integerへの代入の方では元ソースに記述の無いInteger.valueOfが呼び出されているのが確認できる。
これはJava1.5から導入されたオートボクシングという機能で
基本データ型の値を、対応する参照型の変数に変換することを容易にするというもの。
Java1.4まではプログラマが明示的にnew Integer(int型の値)
などと記述する必要があった。
Integer.valueOfの実装
Integerの場合は代入時にInteger.valueOfが呼ばれて値を設定していることがわかったので
次にInteger.valueOfは内部でどういったことをしているのか確認する。
Integer.javaからInteger.valueOfの部分だけを抜粋する。
/** * Returns an {@code Integer} instance representing the specified * {@code int} value. If a new {@code Integer} instance is not * required, this method should generally be used in preference to * the constructor {@link #Integer(int)}, as this method is likely * to yield significantly better space and time performance by * caching frequently requested values. * * This method will always cache values in the range -128 to 127, * inclusive, and may cache other values outside of this range. * * @param i an {@code int} value. * @return an {@code Integer} instance representing {@code i}. * @since 1.5 */ @HotSpotIntrinsicCandidate public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
引数として与えられたintの値がIntegerCacheのlowからhighの間であれば
Integer.IntegerCache.cache
から返し、そうでなければ新しいインスタンスを生成している。
Integer.IntegerCache
次にInteger.valueOf内で利用されているIntegerCacheについて確認する。
同様にInteger.javaからInteger.IntegerCacheの部分だけを抜粋する。
/** * Cache to support the object identity semantics of autoboxing for values between * -128 and 127 (inclusive) as required by JLS. * * The cache is initialized on first usage. The size of the cache * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option. * During VM initialization, java.lang.Integer.IntegerCache.high property * may be set and saved in the private system properties in the * jdk.internal.misc.VM class. * * WARNING: The cache is archived with CDS and reloaded from the shared * archive at runtime. The archived cache (Integer[]) and Integer objects * reside in the closed archive heap regions. Care should be taken when * changing the implementation and the cache array should not be assigned * with new Integer object(s) after initialization. */ private static class IntegerCache { static final int low = -128; static final int high; static final Integer[] cache; static Integer[] archivedCache; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { h = Math.max(parseInt(integerCacheHighPropValue), 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(h, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; // Load IntegerCache.archivedCache from archive, if possible VM.initializeFromArchive(IntegerCache.class); int size = (high - low) + 1; // Use the archived cache if it exists and is large enough if (archivedCache == null || size > archivedCache.length) { Integer[] c = new Integer[size]; int j = low; for(int i = 0; i < c.length; i++) { c[i] = new Integer(j++); } archivedCache = c; } cache = archivedCache; // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} }
メソッドなどの実装はなく、全てstaticイニシャライザで処理が完結している。
CDSでアーカイブされたデータが無ければある範囲の値を生成してキャッシュしていることがわかる。
(java.lang.Integer.IntegerCache.high
の指定がなければ-128~127
の範囲)
Integer.valueOfの実装と合わせて読むと、引数が特定の範囲に入っているケースであれば
毎回新しいインスタンスを生成するのではなく、キャッシュ済みのIntegerを返却している。
そのため、記事の頭にあるコードの場合では
値が100の場合はキャッシュ内から同一のインスタンスが返却されるためtrue
となり
値が200の場合は毎回新しいインスタンスが生成されて返却されるためfalse
となる。
最後に、境界値を含む検証コードを書いて挙動を確認する。
出力結果
true false true false
確かにコードで確認した通りの値域で想定された動作をしていることがわかる。
ちなみに、IntegerだけでなくByte, Character, Longでも同様のキャッシュ機構は存在しており
ByteとLong(-128~127), Character(0~127)の範囲でキャッシュしているが
Integerのようにキャッシュする範囲を変更することはできない。
キャッシュ値域内でも比較を失敗させる方法
オートボクシング時のvalueOfを回避すれば、キャッシュが利用されないため
(当然だが)Integer間の==
での比較は正しく(?)失敗する。
例えば下記のように明示的に同じ値を持つ異なるインスタンスを作成すればよい。
public static void main(String[] args) { Integer a = new Integer(100); Integer b = new Integer(100); System.out.println(a == b); }
出力結果
false
Booleanなどは2値しか無いので当然内部でキャッシュを持っているが
コンストラクタを利用すればtrueかつBoolean.TRUEと異なるインスタンスを作ることが出来る。
結論
Integer.valueOfは一定範囲の値をキャッシュしているため
オートボクシング(Integer.valueOf)経由でIntegerとなった値がその範囲に入った場合
==
での比較にtrueを返してしまうケースがある。
普段の実装ではハマることも無いのでまあそうかという感じではあるものの
たまに何気なく触っているクラスの内部実装を見ると面白い。
有名OSSの中にポケモンがいた話
リポジトリ名をポケモン名に合わせて頭字語にしたとかそういうことではなく
ソースコード内のコメントにアスキーアートでポケモン(ヒトカゲ)がいる。
どこ
Java界隈では有名な Google GuavaというOSSライブラリの
com.google.common.base.CharMatcher.java
内のコメントに存在する。
ちなみに、Google Guavaは2019年7月2日時点で
Star数は32,000以上、Fork数は7,000超えの超有名OSS。
開いた感じだとこう。
かわいいですね。
最初のジムリーダーがいわタイプなので選ぶことはなかったけど、初代っぽいかわいさにあふれています。
仕事中に同僚のコードでCharMatcherを使っている箇所があったので
何するクラスだっけと思って調べたのが見つけたきっかけ。ほっこりした。
なんで
多分Charmander(ヒトカゲの英名)とCharMatcherのスペルが似てるから…?わからん…
任天堂に許可取ってる?みたいなコメントがついてるけど無事(?)マージされたぽい。
2016年の以下のコミットで入ったようだけどマージされた経緯が全くわからない…
おまけ
どのコミットで入ったんやと思って調べるときに下記の拡張を使いました。
すげー便利なので興味ある人はつかってみてね!
abs(int)のコーナーケース
与えられたある整数の絶対値を取得するような
一見シンプルなメソッドにもコーナーケースが存在する。
言語によって挙動が異なるので注意する必要がある。
期待される振る舞い
絶対値を取得するメソッドなので直感的にはこのように動いて欲しい。
$$
abs(x) =
\left\{
\begin{array}{}
x & ( x > 0) \\
0 & ( x = 0) \\
-x & ( x < 0) \\
\end{array}
\right.
$$
言語によって実装は異なるため実際には下記。 $$ abs(x) = \left\{ \begin{array}{} x & ( x > 0) \\ 0 & ( x = 0) \\ -x & ( x < 0) \\ ? & (x = 型で表現できる最小値) \\ \end{array} \right. $$
原因
符号付整数値の内部表現が2の補数となっているため
表現できる幅が負の方向に1だけ広い。
例えば8bit符号付き整数型の場合
最大値は 01111111
127
最小値は 10000000
-128
最小値-128の絶対値である128は8bitで表現できる範囲を超えてしまう。
言語ごとの挙動の違い
1. 最小値をそのまま返却する (C++, Java)
#include <climits> #include <cstdlib> #include <iostream> using namespace std; int main(void) { cout << INT_MIN << endl; cout << abs(INT_MIN) << endl; }
import java.util.*; public class Main { public static void main(String[] args) throws Exception { System.out.println(String.valueOf(Integer.MIN_VALUE)); System.out.println(String.valueOf(Math.abs(Integer.MIN_VALUE))); } }
結果 -2147483648 -2147483648
負数そのまま返しとるやんけ!!!
実際問題、値として最小値が入ってくるケースはごくごく稀なものの
内部的にabsを利用している、みたいな場合に発生すると辛そう。
というか正の値を返して欲しい。
Javaでの内部実装はこちら。
// Math.java内の実装 public static int abs(int a) { return (a < 0) ? -a : a; }
そりゃそうなるかという感じ。
2. 例外を発生させる (C#)
using System; public class IntMin { public static void Main(){ Console.WriteLine(Math.Abs(Int32.MinValue)); } }
結果 Unhandled Exception: System.OverflowException: Negating the minimum value of a twos complement number is invalid. at System.Math.AbsHelper (System.Int32 value) [0x00015] in <8f2c484307284b51944a1a13a14c0266>:0 at System.Math.Abs (System.Int32 value) [0x00009] in <8f2c484307284b51944a1a13a14c0266>:0 at IntMin.Main () [0x00010] in /workspace/Main.cs:6 [ERROR] FATAL UNHANDLED EXCEPTION: System.OverflowException: Negating the minimum value of a twos complement number is invalid. at System.Math.AbsHelper (System.Int32 value) [0x00015] in <8f2c484307284b51944a1a13a14c0266>:0 at System.Math.Abs (System.Int32 value) [0x00009] in <8f2c484307284b51944a1a13a14c0266>:0 at IntMin.Main () [0x00010] in /workspace/Main.cs:6
桁あふれなども含めて例外出して欲しい派なので
静的型付け言語において、個人的に望ましい動作はこっち。
3. 多倍長整数として返却する (Python)
# coding: utf-8 import sys print sys.maxint print -sys.maxint - 1 print abs(-sys.maxint - 1)
結果 9223372036854775807 -9223372036854775808 9223372036854775808
表現できる範囲を超えたら内部的に広げてしまって返す。
内部でどうなっているのか、あまり意識せずに多倍長整数を扱えるのは自然だし便利。
調べた限りだとRubyもそんな感じ。
そもそも
static Random rnd = new Random(); static int random(int n) { return Math.abs(rnd.nextInt()) % n; }
書籍Effective Javaのある項目で、上記コードにはいくつか問題が
あってそれはどこでしょう?という記述に絡めて紹介されていた。
rnd.nextInt()は極めて稀にInteger.MIN_VALUEを返すので
極めて稀に負数が返却されるよって話で、うわ怖ってなって
他言語だとどうなのだろうと思ったのが調べたきっかけ。
確かにリファレンスには注意が書いてあるけど、absなんてどの言語にもあるし
雰囲気で使えちゃうので、知らずに実世界でこのバグ踏んでたらとても辛そう。
引き続き慎重にやっていきましょう。
100万円上限でKyashリアルカードの再発行案内が来た
久しぶりに技術以外の記事。題名通り。
100万円って書いたけど14ヶ月弱メインカードとして使った結果なので
残念ながら日頃からすごい額の散財をしている訳ではない。(したい)
結論から
- Kyashリアルカードにはいくつか制限がある
- 1日の利用限度額5万円
- 1ヶ月での利用限度額12万円
- カード自体の利用限度額100万円
- カード自体の利用限度額に達すると使えなくなるが再発行で対応
- 自分の場合は90万使った時点でメールが来た
- メールから再発行依頼が可能で一週間目処で届く
- 届いた後にその旨返信するとそのタイミングでカードの切替処理が行われる
- そのためいきなり使えなくなることはなさそう(その前にメール等で気づけば)
- 自分の場合は90万使った時点でメールが来た
再発行の流れ
こんな案内メールが来た。
リンクを踏むとtayoriという外部サービスのメールフォームに飛ばされて
そこで住所やらなにやらを入力すると再発行手続きは終わり。
(飛んだ先がkyash.tayori.com
から始まるURLだったので、本物…?となった)
新しいリアルカードが届くのは一週間前後かかるとのこと。
新しいカード受領後に別メールに返信すると、新しい方がアクティブになるとのことなので
それまでは変わらず手元のリアルカードを使い続けられる。
これまで
Kyashを使い始めたのは2018年1月頃からで、最初は仲間内での割り勘などに利用していた。
他にもpaymoというサービスがあって、その時のメンバーに合わせて使い分けていたのだけど
Kyashが2018年6月に2%還元を開始した後はとりあえずネットでの支払いはほぼ全てKyashを通すようにした。
運良くリアルカードが初期に手に入ったため、飲み会での立て替えや普段の買い物などにも利用した結果
時々は月の上限額ギリギリになって、キャッシュバックがおいしい感じになった。
(おいしい様子)
2%還元キャンペーン以降は周囲の人も積極的にKyashを使うようになったので
現金しか使えないようなお店でも、後でKyashで回収みたいな事ができて
物理通貨をやりとりする煩わしさから解放されてかなり楽になった。
Kyash残高が残っているときは優先的にそちらから利用されるのも透過的でかなり使い勝手がいい。
Amazonだったり、近所の薬局やスーパーでKyashを利用するだけで勝手に残高から利用されるので
その体験に慣れてからはKyashに残高がいくら残っているか気にしなくなった。
アカウント凍結問題
あんまり手放しで称賛しまくるのもフェアじゃない(感じがする)ので
一度アカウントが凍結されてしまったことがあったのでその件も載せる。
理由はクレジットカードを変更した際に、名前のスペルが違っていたため(多分)。
片一方は母音を重ねたスペルで、もう一方は一つだけみたいなスペルの違い。
変更後に無事アカウントが凍結されてしまった。
こちらもエンジニアなので「あ〜なるほどですね〜」という気持ちになりながら
想定される理由とかをずらっと書いてサポートにメールを送って祈った。
おもしろ事象によってKyashのアカウントが凍結されてしまいました
— しろめ (@uskey512) September 22, 2018
Kyashのアカウント凍結、システム的にはすごく納得いくので、こういう理由が引っかかったっぽいみたいな連絡をした。タイミング悪くて月末近いので、運が悪いと何かのサービスの決済が失敗するという恐怖に震えている。
— しろめ (@uskey512) September 22, 2018
(凍結してあわあわする様子)
昨日の22時にKyashに凍結解除のお願いメールしたら今日の12時で解除されてて感動していると同時に、休日に仕事させてしまってすまんという申し訳ないお気持ちでいっぱい。
— しろめ (@uskey512) September 23, 2018
やっぱり決済系サービスは分散させとかないとダメか…
と傷心していたところ翌日には解除されてファンになる私(ちょろい)
当時Kyash自体まだすごい大きい組織ではないと記憶していたけど
休日で解決まで24時間かからないのは、中の人すげーなと普通に感動してしまった。
リスク管理の記事とかで『ピンチをチャンスに』とか書いてあると
いや普通にピンチはピンチだろ正気か?とか思ってたけど
この件の対応である種の安心感とか信頼を持ってしまったので不覚にも腹落ちしてしまった。
運営のみなさんへ
Kyashいつもとても便利に使っています!ありがとうございます!
39円送る時に出る隠しアニメーションもかなりイケてるので
自分の携わるプロダクトのどこかでそういう遊び心を入れたいなって思ってます。
今後もガンガン使っていきたいので引き続きよろしくおねがいします。
Qiita/Qiita:Teamの投稿を一括置換するツールをGoで作った
経緯
社で運用している複数のQiita:Teamを統合することになった。
統合にあたり、投稿やプロジェクト自体は問題なく移行できるが記事内のURL参照は自動で置換されないことがわかった。
そのままだと他の投稿への参照が切れてしまうので修正が必要だが膨大な量の投稿を手動でやるのは面倒、というか無理。
作った
Qiita, Qiita:Teamのトークン、置換前文字列、置換後文字列、所属Qiita:Teamドメインを設定すると
これまで自分が書いた全ての投稿の本文内の文字列を置換する。
試したけどできなかったこと
管理者アカウントでの全ユーザー全投稿の一括置換。作業工数的には一番これが理想だったけど無理そう。
もしかするとAPIドキュメントで見落としがあるかもしれないけど、見つけられなかった。
実装メモ
- github管理しているプロジェクトは、
$GOPATH/src/github.com/{user}/{repo}
として置いた方がよい- 更新かけるたびに毎回 go getするハメになって何かおかしいと気づく
- これまでプロジェクト内で他のパッケージを作ったことがなかった
- 更新かけるたびに毎回 go getするハメになって何かおかしいと気づく
- Goをすごい便利でモダンなBetter Cと思ってたのでAPIアクセス大変そうに思ってたけどそうでもなかった
- リクエスト/レスポンスの定義はAPIリファレンスのサンプルオブジェクトをjson-to-goに貼って終わり
- 便利!!!
- リクエスト/レスポンスの定義はAPIリファレンスのサンプルオブジェクトをjson-to-goに貼って終わり
- type hogehoge []structで定義した型の要素でハマった。具体的にはトップレベルが配列のjson。
- hogehogeをパースした後別メソッドにhogehoge[0]を渡したいときどうすれば…
- 配列を内包するオブジェクトを作ってそれをUnmarshalした
- 言語仕様をふわっとしか理解してない弊害が
- hogehogeをパースした後別メソッドにhogehoge[0]を渡したいときどうすれば…
- Qiita API v2の
PATCH /api/v2/items/:item_id
でハマる- エラーメッセージは特になくて、"forbidden"みたいなログしか出ない
- 記事によってtagがある無しでハマってた…?更新対象をtitleとbodyだけにすると解決。
オチ
統合後、Qiita記事同士のリンクはいい感じに対応されていて必要なかった。無念。
それはそうだよね…
どこかで使うことがあれば使おうと思う。多分ない。