コーディングのベストプラクティス(1)-「データオーバーフロー」に注意

4019 ワード

詳細
最近、社内でいくつかの収集と整理の仕事をして、trouble shootingとperformace tuningの中で出会って解決した典型的な問題について、いくつかの内部共有をしました.私は整理して、続々とみんなに分かち合うつもりです.
これらの問題は、単独で見ると、すべての問題は複雑ではなく、深いとは言えないが、実際のプロジェクト開発に現れ、一度は困ったことがあり、一定の普遍性を持っており、具体的には、これらの問題を知らない学生が日常開発で募集しやすいと表現されている.コードベストプラクティスというテーマを開きましたが、名前が少し大きいようです.
まず最初に、compareの作り方を見てみましょう.
ケースを見てみると、問題の表現は簡単ですが、ソート後の結果に驚くことがあります.私たちは具体的なエラー表現の詳細と調査の過程にこだわらず、最終的に問題が検出されたコードを直接見て、これは普通のComparatorインタフェースの実現です.

private static class keyOrderComparator implements Comparator {
    public int compare(Persistent p1, Persistent p2) {
        return (int) (p1.getId().getKey() - p2.getId().getKey());
    }
}

コード中の比較ロジックは簡単で,Persistentオブジェクトのidのkey値を比較すればOKであり,2つのkeyを単純に減算演算し,結果をcompare()メソッドの戻り値とすることを実現する.p 1のkeyがp 2のkeyより大きい場合、「p 1.getId().getKey()−p 2.getId().getKey()」の結果は0より大きく、compareTo()メソッドは、比較結果が「パラメータp 1がパラメータp 2より大きい」ことを示す0より大きい整数を返す.
しかし、面倒はkeyのデータ型に現れ、これはlongタイプであるため、減算の結果もlongであり、compare()メソッドの要求intを返す要求を満たすためにreturn前に強制タイプ変換を行った.問題は、longからintへの強制タイプ変換はリスクがあり、longの数字がintが表す範囲[Integer.Min_VALUE,Integer.Max_VALUE]を超えた場合、「データオーバーフロー」(data overflow)が発生します.
次のコードSystemを実行してみましょう.out.println((int) (30000000000L - 1)); , その結果、「-64771073」という結果になり、予想していた299999999999とは全く異なり、重要なのは記号が変わったことです.正数から負数に変わったことです.これはcompare()メソッドを直接招いて驚くべき比較結果を得た:3000000000は1より小さい!
解決方法も簡単です.強制型変換はしないでください.

private static class keyOrderComparator implements Comparator {
    public int compare(Persistent p1, Persistent p2) {
        long key1 = p1.getId().getKey();
        long key2 = p2.getId().getKey();
	if (key1 == key2) {
	    return 0;
	} else {
	    return key1 > key2 ? 1 : -1;
	}
    }
}

この簡単なケースでは、問題を発見するのに役立つのは、(int)という強制的なタイプ変換であり、少し経験のある学生は最初に反応します.longからintはデータオーバーフローのリスクがあります.もし私たちがこのケースを少し修正したら、p 1と仮定します.getId().getKey()は通常のintを返し、結果はどうなりますか.

private static class keyOrderComparator implements Comparator {
    public int compare(Persistent p1, Persistent p2) {
        return p1.getId().getKey() - p2.getId().getKey();
    }
}

このコードは問題ないようですか?ほほほ、このコードのビジネスの意味を取り除いて、普通のint比較に退化しましょう.

private static class IntegerOrderComparator implements Comparator {
    public int compare(Integer p1, Integer p2) {
        return p1 - p2;
    }
}

これでわかるでしょう?p 1=2174848647がIntegerである場合.MAX_VALUE、p 2=-1、p 1-p 2=Integer.MAX_VALUE - (-1) = -2147483648 ! IntegerOrderComparatorは、2147483647が-1より小さいという呆然とした比較結果を提供します.同様に、p 1=-2174483648(Integer.MIN_VALUE)、p 2=1の場合、IntegerOrderComparatorも同様にでたらめな比較結果を与える:-2174483648は1より大きい!
エラーの原因は依然として「データオーバーフロー」です!前のlongからintへの強制型変換とは異なり,今回のデータオーバーフローはintとintの間で数学的演算を行う.
問題がどこで起こっているのかを見てみましょう.「int-int」のような簡単な演算では、私たちの数学の常識では、2つの整数が減算された結果は肯定的に整数であるか、1つの正数が減算された結果は正数であるか、1つの負数が減算された結果は正数であるか......しかし、ここでの数学常識でいう「整数型」とは、その値の範囲が無限大から無限大であってもよく、java言語(他の言語も同様)のintは、[Integer.Min_VALUE,Integer.Max_VALUE],[-21147483648,2147483647]という範囲しか表さない.演算の結果がこの範囲を超えると、データオーバーフローが発生します.
   
したがってjavaでは,「int+int」,「int−int」,「int*int」のような演算結果をintで表すのは安全ではなく,longのようなより大きなデータ型を用いる必要がある.上のコードは次のように変更できます.

private static class IntegerOrderComparator implements Comparator {
    public int compare(Integer p1, Integer p2) {
        long diff = p1 - p2;
	return diff == 0 ? 0 : (diff > 0 : 1 : -1);
    }
}

しかし、このcompareの書き方は、このdiffの結果を運ぶために、データ範囲の大きいデータ型に遭遇すると依然として面倒である.そのため、減算を行わずに、等しいものと小さいものを直接比較することをお勧めします.
最後にこのケースをまとめます.
1.compareメソッドを実装する場合は、できるだけ「return p 1-p 2」という書き方はしない
2.数値演算を行う場合は、データオーバーフローのリスクに注意する
3.trouble shootingをするときは、可能なデータオーバーフローに注意する
PS:同じ間違いを犯して知らない同級生はいますか.自覚的に爪を残してください、ほほほ