カテゴリー
Java

Java(Step5-2)

基本型と演算

演算と型

整数を整数で割ると、商や剰余が整数として得られますが、実数で求める方法を説明します。

演算と型

2つの整数値を読み込んで、その平均値を表示するプログラムを作りましょう。

//Average1.java
//2つの整数値の平均値を実数で求める(間違い)
import java.util.Scanner;

public class Average1 {
    public static void main(String[] args){
        Scanner stdIn = new Scanner(System.in);

        System.out.println("整数値xとyの平均値を求めます");
        System.out.print("xの値:"); int x = stdIn.nextInt();
        System.out.print("yの値:"); int y = stdIn.nextInt();

        double ave = (x + y) / 2;
        System.out.println("xとyの平均値は" + ave + "です。");
        stdIn.close();
    }
}
Average1.java実行結果

実行例を見ると分かりますが、実数を表すdouble型の変数aveに平均値を入れていますが、7と8の平均が7.5ではんく、7.0となっています。なぜなら、2で割る演算がint / intの結果がint型になり、小数点以下が切り捨てられてしまうためです。

演算中にintとdoubleが混在した場合は、結果が変わってきます。これらの時、2項数値昇格(binary numerical promotion)と呼ばれる型変換(type conversion)が行われます。int型がdouble型に格上げされて演算が行われます。それを確認したのが次のプログラムになります。

//Quotient.java
//2つの数値の商を求める

public class Quotient {
    public static void main(String[] args){
        System.out.println("15 / 2 =" + 15 / 2);
        System.out.println("15.0 / 2.0 =" + 15.0 / 2.0);
        System.out.println("15.0 / 2 =" + 15.0 / 2);
        System.out.println("15 / 2.0 =" + 15 / 2.0);
    }    
}
Quotient.java実行結果

double型は小数点以下の部分を格納する点でint型に比べ、余裕があります。具体的には、2項数値昇格における型変換は以下のようになっています。

  • 一方のオペランドがdouble型ならば、他方をdouble型に変換する
  • そうでないなら、一方のオペランドがfloat型ならば、他方をfloat型に変換する
  • そうでないなら、一方のオペランドがlong型ならば、他方をlong型に変換する
  • そうでなければ、両オペランドをint型に変換する

以上を踏まえ、平均値を実数値で求めるプログラムを改良すると以下のようになります。

//Average2.java
//2つの整数値の平均値を実数で求める(合計を2.0で割る)
import java.util.Scanner;

public class Average2 {
    public static void main(String[] args){
        Scanner stdIn = new Scanner(System.in);

        System.out.println("整数値xとyの平均値を求めます");
        System.out.print("xの値:"); int x = stdIn.nextInt();
        System.out.print("yの値:"); int y = stdIn.nextInt();

        double ave = (x + y) / 2.0;
        System.out.println("xとyの平均値は" + ave + "です。");
        stdIn.close();
    }
}
Average2.java実行結果

キャスト演算子

普段の生活では、数字を割る時「2.0で割ろう」とは考えず、「2で割ろう」と考えます。2つの整数をはじめに実数に変換し、それを2で割ることで平均値を求めるようにしましょう。それが次のプログラムになります。

//Average3.java
//2つの整数値の平均値を実数で求める(キャスト演算子を利用する)
import java.util.Scanner;

public class Average3 {
    public static void main(String[] args){
        Scanner stdIn = new Scanner(System.in);

        System.out.println("整数値xとyの平均値を求めます");
        System.out.print("xの値:"); int x = stdIn.nextInt();
        System.out.print("yの値:"); int y = stdIn.nextInt();

        double ave = (double)(x + y) / 2.0;
        System.out.println("xとyの平均値は" + ave + "です。");
        stdIn.close();
    }
}
Average3.java実行結果

平均値を求める部分が、以前のプログラムと異なります。演算子 / の左オペランドの式(double)(x + y)の形式を一般的に表すと次のようになります。

(型) 式

これは、式の値を型としての値に変換したものを生成するための式になります。この型変換をキャスト(cast)と呼びます。( ) は優先的に演算を行うための記号ではなく、キャスト演算子(cast operator)と呼ばれる演算子になります。

基本型の縮小変換

前のプログラムでは、int型の値をdouble型として取り出すためにキャスト演算を利用しました。次は逆の変換について考えてみましょう。

そこで、重要になるのがより小さい型への値の代入にはキャストが必須となることです。

次の変換がキャストが必須となる基本型の縮小変換(narrowing primitive conversion)となります。変換に伴い、数値の大きさの情報や精度が失われることがあります。

  • short から byte、char への変換
  • char から byte、short への変換
  • int から byte、short、char への変換
  • long から byte、short、char、int への変換
  • float から byte、short、char、int、long への変換
  • double から byte、short、char、int、long、float への変換
定数の代入・定数による初期化

基本型の縮小変換では、原則としてキャストが必要ですが、例外もあります。以下のようなケースです。

byte a = 0;
a = 5;
short b = 53;

代入の右辺の式や初期化子がbyte、short、char、int型の定数式で、代入先あるいは初期化先の変数の型がbyte、short、charであって、定数式の値が変数の型で表現できる場合は、基本型の縮小変換が自動で行われます。

基本型の拡大変換

縮小変換の逆が、基本型の拡大変換(widening primitive conversion)と呼びます。以下に示す種類があります。

  • byte から short、int、long、float、double への変換
  • short から int、long、float、double への変換
  • char から int、long、float、double への変換
  • int から long、float、double への変換
  • long から float、double への変換
  • float から double への変換

この変換は代入か初期化の際に自動的に行われます。基本型の拡大変換では、基本的に数値の大きさの情報が失われません。しかし、以下の変換では精度を失うことがあります。

  • int あるいは long の値から float への変換
  • long の値から double への変換

この場合の浮動小数点の変換結果は、最も近い値に丸められた整数値になります。例となるプログラムを以下に示します。

//IntegralToFloat.java
//整数型から浮動小数点型への変換(精度が失われる例)
public class IntegralToFloat {
    public static void main(String[] args){
        int a = 123456789;
        long b = 1234567890123456789L;

        System.out.println(" a = " + a);
        System.out.println("(float) a =" + (float)a);
        System.out.println(" b = " + b);
        System.out.println("(double) b =" + (double)b);
    }
}
IntegralToFloat.java実行結果

基本型の拡大変換と縮小変換

以下の変換は、基本型拡大変換と縮小変換の2段階で行われます。

  • byte から char への変換

まず、byteが基本型の拡大変換でintに変換され、その後、縮小変換によってintからchar型に変換されます。

繰り返しの制御

次のプログラムは、float型の変数xの値を0.0から1.0まで0.001ずつ増やしながら表示して、最後に合計を表示するプログラムになります。

//FloatSum1.java
//0.0から1.0まで0.001単位で増やして合計を表示(繰り返しをfloat制御)
public class FloatSum1 {
    public static void main(String[] args){
        float sum = 0.0F;

        for(float x = 0.0F; x <= 1.0F; x += 0.001F){
            System.out.println("x = " + x);
            sum += x;
        }
        System.out.println("sum =" + sum);
    }
}
FloatSum1.java実行結果

最後のxの値が1.0ではないことに気を付けましょう。浮動小数点数が、すべての桁の情報を失うことなく、表現できるとは限らないということです。そのため、1000個分の誤差がxに累積します。

繰り返しの制御を整数で行うように書き直したプログラムを以下に示します。

//FloatSum2.java
//0.0から1.0まで0.001単位で増やして合計を表示(繰り返しをint制御)

public class FloatSum2 {
    public static void main(String[] args){
        float sum = 0.0F;

        for(int i = 0; i <= 1000; i++){
            float x = (float)i / 1000;
            System.out.println("x = " + x);
            sum += x;
        }
        System.out.println("sum =" + sum);
    }
}
FloatSum2.java実行結果

for文では、変数iの値を0から1000までインクリメントします。繰り返しの度にiを1000で割った値をxとします。xが目的とする実数値をピッタリと表現できるわけではありません。毎回xの値を求め直すので、誤差は累積しません。