🍇

3.3 変数と型に関する様々な言語仕様(キャスト、型サフィックス、型推論など)~Java Basic編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、Udemy講師の斉藤賢哉です。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Basic編』の一部の範囲をカバーしたものです。『Java Basic編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの言語仕様や文法を正しく理解すると同時に、現場での実践的なスキル習得を目指している方
  • 新卒でIT企業に入社、またはIT部門に配属になった、新米システムエンジニアの方
  • 長年IT部門で活躍されてきた中堅層の方で、学び直し(リスキル)に挑戦しようとしている方
  • 今後、フリーランスエンジニアとしてのキャリアを検討している方
  • Chat GPT」のエンジニアリングへの活用に興味のある方
  • Oracle認定Javaプログラマ」の資格取得を目指している方
  • IT企業やIT部門の教育研修部門において、新人研修やリスキルのためのオンライン教材をお探しの方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

3.3 変数と型に関する様々な言語仕様

チャプターの概要

このチャプターでは、プリミティブ型変数と文字列型変数に関連する様々な言語仕様を、さらに深掘りして学びます。

3.3.1 変数同士の代入と書き換え

変数同士の代入

同一の型を持つ2つの変数があった場合、変数から変数に代入することにより、値をコピーすることができます。
まずはint型からです。

snippet_1 (Main_3_3)
int x = 10;
int y = x; // 変数yに10が代入される

このようにすると、変数yには10が代入されます。
次にString型です。

snippet_2 (Main_3_3)
String str1 = "foo";
String str2 = str1; // 変数str2の文字列は"foo"

このようにすると、変数str2には文字列"foo"が代入されます。

代入元変数の書き換え

一度変数に値を代入した後、代入の変数の値を書き換えると、代入の変数の値はどうなるでしょうか。
まずint型の例です。

snippet_3 (Main_3_3)
int x = 10;
int y = x; // 変数yに10が代入される
x = 20;
System.out.println(y); // どうなる?

このコードを実行すると10が表示されます。このようにプリミティブ型変数同士の代入では、代入した後に代入の変数を書き換えても、代入の変数は変化しません。
次にString型の例です。

snippet_4 (Main_3_3)
String str1 = "foo";
String str2 = str1; // 変数str2の文字列は"foo"
str1 = "bar";
System.out.println(str2); // どうなる?

このコードを実行すると、文字列"foo"が表示されます。このようにString型変数同士の代入もプリミティブ型と同様に、代入した後に代入の変数を書き換えても代入の変数は変化しません。
このようなString型の挙動は、当たり前のように思えるかもしれませんが、必ずしもそうとは限りません。String型のような参照型変数では、データそのものではなく、実データのメモリ上の配置場所への「参照」を表すため、通常は代入変数を書き換えると連動して代入変数も変更されます。それでもString型がこのような挙動をするのは、Stringクラスに固有の特徴[1]によるものです。

3.3.2 型に関する様々な言語仕様

型チェック

Javaは静的型付け言語です。静的型付け言語では、プログラムを実行する前にコンパイラによって変数の型がチェックされます。宣言したデータ型とは異なる値を、当該の変数に代入しようとすると、コンパイルエラーになります。
例えば文字列"abc"をint型変数xに代入することも、数値10をString型変数strに代入することも、できません。

snippet_5 (Main_3_3)
int x = "abc"; // コンパイルエラー
String str = 10; // コンパイルエラー

このように「変数の型がコンパイラによって保証される」点がJavaの大きな特徴であり、言語としての堅牢性向上に大きく寄与しています。

型変換

前述したようにJavaではコンパイラによって変数の型がチェックされますが、型と型の間に互換性がある場合に限り、型を変換することが可能です。このような型変換のことを「キャスト」と呼びます。型変換には、暗黙的(自動的)に行われるケースと、開発者が明示的に行うケースがあります。

数値型の型変換(1):拡大変換

プリミティブ型の数値型には、整数型と浮動小数点型がありますが、ここではこれら数値型の型変換について説明します。数値型において、値の範囲が狭い型から広い型へと代入することを、拡大変換と呼びます。拡大変換では、データ型の変換は暗黙的(自動的)に行われます。
例えば以下のコードのように、int型変数の値はそのままlong型変数に代入することができます。

snippet_6 (Main_3_3)
int x = 100;
long y = x;

また同じように、float型変数の値はそのままdouble型変数に代入することができます。

snippet_7 (Main_3_3)
float f = 12345.67F;
double d = f;

数値型の型変換(2):縮小変換

数値型において、値の範囲が広い型から狭い型へと代入することを、縮小変換と呼びます。縮小変換では、何もしないと以下のコードのようにコンパイルエラーになります。

snippet_8 (Main_3_3)
int x = 100;
short y = x; // コンパイルエラー

このときキャスト構文によって明示的に型変換を行うと、コンパイルエラーは解消します。以下のコードを見てください。

snippet_9 (Main_3_3)
int x = 100;
short y = (short) x; // コンパイルエラーは解消

このコードにおける( )の部分はキャスト演算子と言われており、明示的に型変換する場合に使用します。

【構文】キャスト演算子による型変換
(データ型) 変数名;

このように、型変換対象の変数の前に、変換先のデータ型を( )で囲って指定します。( )の後ろのスペースは任意ですが、可読性確保の観点から通常はこの位置にスペースを入れます。
ただしキャスト構文による縮小変換は、値が変換後のデータ型の範囲内であることを、開発者自身で保証することを前提にしています。値が変換後のデータ型の範囲を超えている場合は、思わぬ結果を招く可能性があります。
例えば以下のコードのように、int型の値400を縮小変換によってbyte型変数に代入するケースを考えます。

snippet_10 (Main_3_3)
int x = 400;
byte y = (byte) x;

このコードを実行すると「桁あふれ」が発生し、結果的に変数yには数値-112が格納されます。このような「桁あふれ」のメカニズムについては、レッスン3.1.2を参照してください。
またキャスト構文によって、浮動小数点型を整数型に縮小変換することも可能ですが、その場合は「情報落ち」が発生し精度が失われます。以下のコードを見てください。

snippet_11 (Main_3_3)
double d = 12345.67;
int x = (int) d;

このコードを実行すると、小数点以下は切り捨てられ、変数xの値は12345になります。

型サフィックス

Javaでは、整数型、浮動小数点型のそれぞれについて、数値リテラルに対応するデータ型が自動的に決めらます。整数リテラルの場合はint型、浮動小数点リテラルの場合はdouble型が、それぞれのデフォルトになります。従ってlong型やfloat型の変数に値を代入する場合は、型サフィックスにより、明示的にデータ型を指定する必要があります。型サフィックスには、long型を表すLとfloat型を表すFなどがあります。
例えば、以下のコードを見てください。

snippet_12 (Main_3_3)
long x = 100;

これは問題なくコンパイルされ、値も正しく保持されます。100は整数リテラルのため、int型として認識されますが、その値は暗黙的な変換によってlong型の変数xに問題なく代入されます。
ただし、以下のコードはコンパイルエラーになります。

snippet_13 (Main_3_3)
long x = 3_000_000_000; // リテラル30億はint型の範囲外

30億という整数が、整数リテラルのデフォルトであるint型の値の範囲を超えてしまっているから[2]です。このような場合は整数の末尾に型サフィックスであるLを付与するとlong型であることを明示できるため、コンパイルエラーは発生しません。

snippet_14 (Main_3_3)
long x = 3_000_000_000L;

このようにlong型を利用する場合は、値がint型の範囲内であるかどうかに関わらず、常にLを付与すると良いでしょう。
次にfloat型の型サフィックスです。浮動小数点リテラルをfloat型に代入する場合は、型サフィックスであるFを付与し、float型であることを明示する必要があります。具体的には以下のようなコードになります。

snippet_15 (Main_3_3)
float f = 12345.67F;

ローカル変数の型推論

Javaでは、varキーワードによって変数宣言をすることで、個別にデータ型を宣言する必要がなくなりました。代入された値からコンパイラが自動的にデータ型を推論し、決定します。これは型推論と呼ばれる機能で、Java 10でサポートされました。ただし型推論は、ローカル変数にしか使うことができません(フィールドには使用不可)。

【構文】varキーワードによる型推論
var 変数名 =;

具体的には以下のコードを見てください。

snippet_16 (Main_3_3)
var x = 10; // int型と推論される

このように記述すると、変数xは、代入された10という値から自動的にint型に決定されます。
double型やString型についても同様です。

snippet_17 (Main_3_3)
var d = 12345.67; // double型と推論される
var str = "foo"; // String型と推論される

varキーワードによる型推論には、幾つかの制約があります。まず宣言と初期値の代入は、同時に行う必要があります。また1つの命令文内で、複数の変数を宣言することもできません。従って、以下のコードの各行はいずれもコンパイルエラーになります。

snippet_18 (Main_3_3)
var x; // コンパイルエラー
var x = 10, y = 20; // コンパイルエラー

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. 変数から変数へのコピーを行ったとき、どういった挙動になるのか。
  2. 型変換には、暗黙的なものと明示的なものがあること。
  3. 数値型の拡大変換と縮小変換、および縮小変換の注意点について。
  4. 整数型や浮動小数点型のデータ型を表すための、型サフィックスについて。
  5. varキーワードによるローカル変数の型推論について。
脚注
  1. Stringクラスのこのような挙動について、詳細はチャプター16.1で取り上げる。 ↩︎

  2. コンパイルエラーの発生は整数リテラルがint型の範囲を超えていることが原因であり、「int型変数にそれを超える範囲の整数を代入できない」という意味ではない。例えばint x = (int) 3_000_000_000Lといった具合にキャスト演算子を使えば、int型を超える範囲の整数を代入可能だが、この場合は「桁あふれ」が発生する(レッスン3.1.2参照)。 ↩︎

Discussion