公開文書‎ > ‎

プログラミング言語と数字


WEB+DB PRESS Vol.43 の記事です。


一口に「数字」と言っても、プログラミング言語によって扱い方は違います。この章では業務でよく使われているJavaや最近話題のRubyなどのプログラミング言語の実例を交えながら、数字の扱いをみてみます。


固定長整数


JavaやC言語では整数は固定長で表現されます。Javaのプリミティブ型のintは、32bitの固定幅です。32bitでの表現を超える整数を扱うことはできません。Javaで32bitを超える整数を扱いたい場合はlongを使います。longを使うことで、64bitで表現できる整数を扱うことができますが、それよりもさらに大きな整数を扱うことはできません。

32bitのintでは、-2,147,483,648から2,147,483,648までの整数を扱うことができます。この32bitの境界線上にまたがる演算では奇妙なことがおこります。リスト1のJavaのコードを実行してみてください。このコードは、Integer.MAX_VALUEの値(2,147,483,648)とInteger.MAX_VALUEに1を足した値を出力しています。Integer.MAX_VALUEに1を足すと、Integer.MIN_VALUEの値(-2,147,483,648)になります。符号付き整数では、最上位bitがプラスかマイナスかのフラグになっています。1を足すと全てビットがたちます。最上位ビットがたつことで、JavaのVMはこれをマイナスの値だと認識します。

リスト1
 public class NumberTest {
     public void main(String[] argv) {
         System.out.println(Integer.MAX_VALUE + Integer.MaxValue);
     }
 }

次に、Integer.MAX_VALUE同士を足し合わせるとどうなるでしょうか?リスト1のコードを実行すると、答えは-2(図1)になります。Javaを代表とする多くのプログラミング言語では、bitをあふれた場合、あふれたbitは切り捨てられます。そのため、32bitの境界線上での演算は予測不能なものとなります。

図1
 C:> java NumberTest
 -2
 C:>


可変長整数

32bitで表せない整数を扱う場合は、あらかじめlongを使う必要があります。しかし、longを使っても64bitを超える整数を扱うことはできません。また、Javaではintを自動的にlongに拡張する仕組みもありません。

Rubyでは整数を扱う場合、ビット長は自動的に拡張されます。整数はデフォルトでは32bitですが、桁数が足りなくなると自動で整数のビット長を拡張します。プログラマは整数のビット長を意識することなく32bitから64bitに拡張できます。また、Rubyではビット長を固定で持っているわけではなく、必要に応じてメモリの許す範囲内で拡張できます。

リスト2は32bitでの最大値2147483647に1を足したものです。短いプログラムなので、コマンドライン引数にワンライナーで記述して実行しています。リスト3は、リスト2とほとんど同じですが64bitでの最大値9223372036854775807を掛け合わせています。両方の実行結果から、32bitや64bitの範囲を超えて大きな数字を扱えることがわかると思います。

リスト2
 C:> ruby -e "print 2147483647 +1;"
 2147483648
 C:>

リスト3
 C:> rruby -e "print 9223372036854775807*9223372036854775807;"
 85070591730234615847396907784232501249
 C:>


浮動小数点と丸め誤差


第1章で説明したように、浮動小数点の表現方法はIEEE???で標準化されています。しかし、浮動小数点の演算では不思議な振る舞いをします。この振る舞いを丸め誤差と呼びます。「1.0 - 0.9」と「1.0 / 10」は、どちらも同じ値のように見えます。リスト4は、Javaで行うコードです。図2がリスト4の実行結果です。この違いが丸め誤差で2進数での演算が原因です。このため、演算を行った結果のdoubleの数値同士を比較するようなロジックは、誤差の影響があるため危険です。

リスト4
 class DoubleTest {
     public static void main(String[] argv) {
         double d1 = 1.0 - 0.9;
         double d2 = 1.0/10.0;
         
         System.out.println(d1);
         System.out.println(d2);
     }
 }

図2
 C:> java DoubleTest
 0.09999999999999998
 0.1
 C:>

では、この演算をRubyで行うとどうなるでしょうか?リスト5はrubyから実行したときの結果です。両方の演算の結果は0.1で期待通りの動作になります。Rubyは優秀ということでしょうか?では、Rubyで演算結果を比較してみます。リスト6は「1.0 - 0.9」と「1.0 / 10」を比較して真偽値を出力しています。結果はfalseになりました。演算結果の出力では、正しく演算されたように見えますが、真偽値の判定ではRubyの内部データの値を厳密に評価するためにfalseになります。

リスト5
 C:> ruby -e "print 1.0-0.9"
 0.
 C:> ruby -e "print 1.0/10.0"
 0.1
 C:>

リスト6
 C:> ruby -e "print (1.0/10.0) == (1.0 - 0.9);"
 false
 C:>


Javaで浮動小数点演算で丸め誤差を制御するためには、java.math.BigDecimalを使います。BigDecimalは指定した精度で少数を表現できます。また、演算時に丸め操作のモードを指定することで、丸め誤差を制御できます。

リスト7はリスト4をBigDecimalを使って書き直したものです。このコードを実行すると、「1.0 - 0.9」も「1.0 / 10」も同じ結果が得られました (図3) 。ただし、計算対象の桁数を意識する必要があります。桁数はBigDecimalのコンストラクタの引数の桁数で制御します。

リスト7
import java.math.*;
public class BigDecimalTest {
    public static void main(String[] args) {
        BigDecimal i1 = new BigDecimal("1.0");
        BigDecimal i2 = new BigDecimal("1.0");
        
        System.out.println(i1.divide(new BigDecimal("10.0")));
        System.out.println(i2.subtract(new BigDecimal("0.9")));
    }
}

図3
 C:> java BigDecimalTest
 0.1
 0.1
 C:>


真偽値の扱い

Javaでは、条件文の判定は真偽値(boolean型)で行われます。boolean型はtrueかfalseです。プログラミングを初めた言語がJavaの人はあたりまえのように感じるかもしれません。Javaが普及する以前に人気のあったC/C++では、条件文の判定は、0か0以外かで行われていました。リスト?はC言語での条件文です。Linuxのgccでコンパイルして実行すると、図4のように0以外の値はすべて「true」になります。

リスト8
#include <stdio.h>
 
 int main(int argc, char **argv) {
     int i;
     for (i=0; i<3; i++) {
         if (i) {
             printf("true  : %d\n", i);
         } else {
             printf("false : %d\n", i);
         }
     }
     return 0;
  }


図4
 $ ./a.out
 false : 0
 true  : 1
 true  : 2
 nbsp;

リスト9はリスト8のコードをJavaで書き直したものです。C言語のように「if (i)」と記述すると図?のようにコンパイルエラーが出力されてコンパイルできません。Javaでは真偽値と数字が明確に区別されています。

 
リスト9
 public class BoolTest {
     public static void main(String[] argv) {
         for (int i=0; i<3; i++) {
             if (i!=0) {
                 System.out.println("true  : " + i);
             } else {
                 System.out.println("false : " + i);
             }
         }
     }
  }

図5
 C:> javac BoolTest.java 
 BoolTest.java:4: 互換性のない型
 検出値  : int
 期待値  : boolean
              if (i) {
                  ^
 エラー 1 個

RubyでもJavaと同様に数字を条件文の判定に使うことはできません。Rubyではすべてのデータがオブジェクトです。Javaではintやdoubleのようなプリミティブ型が存在します。オブジェクト指向のJavaにとって、プリミティブ型は例外的な型です。RubyではJavaのプリミティブ型は存在せず、すべてがオブジェクトです。数字の0も実体を持ったオブジェクトです。このため、真偽値の判定で、0も「オブジェクトがある状態」と判定されます。リスト?のコードを実行すると、すべて「true」になります。

リスト10
 $ ruby -e '
 (0..2).each do |i|
   if i then
       printf "true  : %d\n", i;
   else
       printf "false : %d\n", i;
   end
 end
 '
 
 true  : 0
 true  : 1
 true  : 2
 $


Javaでのプログラミング時に、メモリ上にオブジェクトがどのように割り当てられているかを意識している人は少ないと思います。オブジェクトを生成するとメモリ上にオブジェクトが配置されます。C言語ではJavaよりもメモリの状態を意識する必要があります。C言語でメモリ上のオブジェクトの位置を表すものがポインタです。ポインタも数字で表されます。Javaのnull値は、C言語の多くの実装系では0に割り当てらています。null値の判定もC言語ではリスト?のように記述できます。ただし、実装系によってはnullが0以外の場合もあるので、「obj != null」のように記述した方が安全です。

リスト11
 void some_function(some_object_t *obj) {
     if (obj) {
         // objがnullでない場合の処理
     } else {
         // objがnullの場合の処理
     }
 }




文字列と数字の変換

まず、リスト12のJavaのコードを見てください。これは、文字列に数字を足し合わせて、標準出力に結果を表示しています。文字列に数字を足し合わせるときに暗黙の型変換が行われています。Java VMの中でおこっていることは、まず、プリミティブ型の3がIntegerのオブジェクトとして生成されて、toString()メソッドが呼び出されて文字列(String型)になります。このように、単純に文字列と数字を連結するように見えて、内部では多くの処理が行われています。

リスト12
 public Test {
    public static void main(String[] argv) {
        System.out.println("hoge " + 3)
    }
 }

Rubyでは、文字列をそのまま数字と連結することはできません。明示的に数字を文字列に変換する必要があります。リスト13はRubyで文字列に数字を連結しています。Rubyでも数字はオブジェクトです。このため、メソッドを持つことができます。to_sはオブジェクトを文字列で表現するためのメソッドです。

リスト13
C:> ruby -e "print 'hoge ' + 3.to_s"
 hoge 3
 TODO:>

次に、PHPについてみてます。PHPでは暗黙の型変換が激しい言語です。文字列で表現された数字を、実際の数値と比較するときには自動的に数字に変換されます。リスト14は、数字の0と文字列の0を比較した結果を出力しています。文字列の「0」が暗黙に数字に変換されて0になり、真偽値を比較して1になります。

リスト14
 $php -r "print (0 == '0');"
 1
 $

では、空文字同士と0を比較するとどうなるでしょうか?リスト15のコードを実行してみてください。結果は1になります。PHPでは暗黙の型変換をせずにオブジェクトの値通しを比較したい場合は「===」を使う必要があります。

リスト15
 $ php -r "print (0 == '');"
 1
 $


Comments