リテラルとは、プログラム中に値を直接記述する表現、あるいはその値そのもののことです。Javaでは、プリミティブ型と参照型、値型と参照型という言い方もありますが、int, float, booleanのようなプリミティブ型をプログラム中に記述する際に、int i = 0;
のように代入する値0をリテラルで書きます。主にプリミティブ型をリテラルで表記しますが、ダブルクォーテーションで囲われた文字列やnullのような特殊な参照型もリテラルが存在します。Scalaは全ての型が参照型でありプリミティブ型は存在しませんが、Javaに存在するリテラルはScalaでも同様に存在します。プリミティブ型はスタック領域に保持され、参照型、は参照はスタック領域に、インスタンスはヒープ領域に保持されます。Stringクラスのinternメソッドを使用した場合のインスタンスは、Java 6まではPermanent領域に保持され、Java 7からはヒープ領域に保持されるようになりました。なお、本書ではStringクラスのinternメソッドもそれに類するScalaのSymbolクラスやSymbolリテラルについても取り上げません。(一般的に、Symbolやinternは文字列処理というよりはマップのキーの管理の文脈で登場する技術です。)
非ヒープ領域(Permanent領域やMetaspace領域)によるOutOfMemoryErrorについてはコラム:非ヒープ領域によるOutOfMemoryError、OutOfMemoryErrorやStackOverflowErrorの対処法については コラム:OutOfMemoryErrorやStackOverflowErrorの対処法を参照ください。
Scalaで文字を管理するクラスはCharクラス、文字列を管理するクラスはStringクラスです。 Javaのプリミティブ型charを、Scalaにはプリミティブ型が存在しないため、参照型のCharクラスが管理します。 Stringクラスは、Charクラスの配列を扱うラッパークラスです。 ラッパークラスというのは、データとそのデータを扱うために便利なメソッドを保持するクラスのことです。 ScalaのCharクラスのサイズはJavaのchar型と同様に16bitです。 Javaのchar型はUnicodeの発案に基づいて16bitに設計されましたが、後にサロゲートペアが登場し、全ての1文字を1つのchar型で表せないことが判明しました。 そのため、Javaのchar型を同じサイズで引き継いだScalaのCharクラスもまた全ての1文字を1つのCharクラスで表せるわけではありません。
なぜ1文字が1つのchar型で表せなくなったのかについて文字コードの歴史を振り返るにはコラム:世界統一文字コードの歴史 〜なぜ1文字が1つのCharで表せなくなったのか〜を参照ください。
JavaとScalaの文字に関連するリテラルの対照表です。表が示すように基本的にはリテラルの表記方法はJavaとScalaに違いはありません。しかし、Javaには存在しないがScalaには存在する生文字リテラルがあります。
Scalaのvalとvarの使い分けはコラム:Scalaのvalとvarの使い分け、日本語の半角円記号とバックスラッシュ記号の混合問題についてはコラム:日本語の半角円記号とバックスラッシュ記号の混同問題を参照ください。
@Test
def testLiteral(): Unit = {
//空リテラル
//Java:
//Object n = null;
//Scala:
val n: Object = null
//文字リテラル
//Java:
//char c = ‘A’;
//Scala:
val c: Char = 'A'
//文字列リテラル
//Java:
//String s = "AB¥¥C¥nあいう";
//Scala:
val s1: String = "AB\\C\nあいう"
//生文字リテラル
//Java:
//なし
//Scala:
val s2: String = """AB\C
あいう"""
}
文字リテラルや文字列リテラルで\記号が必要なエスケープシーケンスを生文字リテラルでは\記号なしで表記できます。ただし、Unicodeシーケンスの\記号は生文字リテラルでも必要です。
@Test
def testRawStringLiteral(): Unit = {
val sStringLiteral: String =
if (System.getProperty("os.name").toLowerCase.startsWith("windows")) {
//もしWindows上で実行する場合は次
"AB\\C\r\nあいう"
} else {
//もしUnix上で実行する場合は次
"AB\\C\nあいう"
}
val sRawStringLiteral: String = """AB\C
あいう"""
val waveDashStringLiteral: String = "\u301c"
val waveDashRawStringLiteral: String = """\u301c"""
assert(sStringLiteral.toString == sRawStringLiteral)
assert(waveDashStringLiteral == waveDashRawStringLiteral)
}
文字列の一致についてはコラム:文字列の一致、文字の一致についてはコラム:文字の一致を参照ください。
改行文字を生文字リテラルで扱うとプログラムのインデントが崩れてしまい可読性が低下する問題が発生します。stripMarginメソッドを使用すると改行文字を含む生文字リテラルのインデントを揃えることができます。
@Test
def testStripMargin(): Unit = {
val s: String = """AB\C
あいう
イロハ"""
val s1: String = """AB\C
|あいう
|イロハ""".stripMargin
val s2: String = """AB\C
%あいう
%イロハ""".stripMargin('%')
assert(s == s1)
assert(s == s2)
}
static変数はJava 7までは非ヒープ領域であるPermanent領域に保持されますが、Java 8からPermanent領域が廃止され、static変数はヒープ領域に保持されるようになりました。Permanent領域の他の機能は非ヒープ領域であるMetaspace領域が引き継ぎました。OutOfMemoryErrorをはいてシステムが停止した場合、ヒープ領域とPermanent領域のどちらがいっぱいになったのか調べるなんてことを経験したことがあるかもしれませんが、容量の上限値の初期値が小さいPermanent領域が廃止され、容量の上限値の初期値が64bitプロセッサが取り扱えるメモリの上限値に設定されているMetaspace領域に移行したことで非ヒープ領域からのOutOfMemoryErrorの可能性をあまり意識しなくて済むようになりました。
OutOfMemoryErrorやStackOverflowErrorを回避するには、大まかには(1)プログラムで使用するメモリ容量を減らすか、(2)物理的に割り当てる容量を変更するかの2つあります。
プログラム上で使用するメモリ容量を減らすためには、まずどの処理がどの領域のメモリをどのくらい使用しているのかを把握しボトルネックを探します。そしてボトルネックを改善するためにアルゴリズムを変更して空間計算量複雑性を下げます。
どの処理がどの領域のメモリをどのくらい使用しているのかを探るためには次のようなツールが便利かもしれません。
Java VisualVMやJConsoleでグラフィカルに確認することができます。端末からそれぞれ次のコマンドで実行してみてください。
jvisualvm
jconsole
RuntimeクラスやMemoryUsageクラスを使用して、メモリの使用状況をモニターする方法もあります。
JVMオプション (Windows, Unix)で-verbose:gc, -Xloggc:filenameや-XX:+PrintGCDetailsを与えることで、OutOfMemoryErrorの原因になるFull GCの発動回数をモニタし、JVMオプションで-XX:+PrintHeapAtGCでGC発動前後のヒープ領域の使用状況をモニタする方法もあります。
時間計算量複雑性と空間計算量複雑性はトレードオフの関係になりがちです。空間計算量複雑性を下げるために時間計算量複雑性が上がることはしばしば起こります。両方の計算量複雑性をともに下げるには動的計画法のような漸化式的な複雑なアルゴリズムを適応したり、より細かい枝刈りをしたり、バイナリレベルでデータ操作しメモリの番地を意識するようなよりlower levelのアルゴリズムが必要になるかもしれません。それによりプログラムの可読性が下がりがちです。漸化式的なアルゴリズムは再帰関数で書かれるためにStackOverflowErrorの発生する危険性が上がります。ただし、末尾再帰という書き方で書くことでこの問題は回避できます。詳しくは、コラム:逆アセンブリ・逆コンパイルの逆コンパイルの節中に解説が含まれていますのでご参照ください。
オンメモリでのアルゴリズムによる改善が難しい場合、一部をHDDやSSDのようなストレージに載せる方法を考えます。その際、ストレージのFile IOが遅いことが問題になるかもしれません。その場合は、読み込むファイルの読み込む位置を先頭からではなく途中から読み込むRandomAccessFileを使用することや何度も読み込むデータをWeakHashMapでキャッシュしたりといった対策が考えられます(ちなみに、無視してくださって問題ないですがメモとして、concurrentなWeakHashMapはGuavaを用いると容易に作成できます)。
JVM始動時にJVMオプションによって確保するメモリの各領域の容量を変更することができます。
JVMが始動時に確保するメモリの各領域の容量を変更するJVMオプション(1)
スタック領域 | ヒープ領域 | |
---|---|---|
容量 | -Xss | -Xms |
最大容量 | -Xss | -Xmx |
JVMが始動時に確保するメモリの各領域の容量を変更するJVMオプション(2)
ヒープ領域内のNew領域 (のOld領域に対する比率) |
New領域内のEden領域 (のSurvivor領域に対する比率) |
|
---|---|---|
容量 | -Xmn -XX:NewSize= |
|
最大容量 | -Xmn -XX:MaxNewSize= |
|
比率 | -XX:NewRatio= | -XX:SurvivorRatio= |
JVMが始動時に確保するメモリの各領域の容量を変更するJVMオプション(3)
Metaspace領域 | Permanent領域 | |
---|---|---|
容量 | -XX:MetaspaceSize= | -XX:PermSize= |
最大容量 | -XX:MaxMetaspaceSize= | -XX:MaxPermSize= |
もし、メモリの容量が不足して領域に新たに容量を割り当てられない場合は、仮想記憶で見かけ上の主記憶の容量を増やす方法があります。もしくは、多少お金を払っても構わない人は、Amazon AWSやHeroku、Windows Azure、Google Compute Engine、IBM SoftLayer、さくらのクラウド、Conoha VPS、さくらのVPSなどIaaSの導入を検討してみてはいかがでしょうか。計算機自体を変える場合でも、単純に使用する計算機1台のスペックを向上させるスケールアップと分散化して処理するスケールアウトのどちらで対応するかの選択肢があります。
機械語コードを人間の読めるコードに変換することで、機械語コードが最適なコードになっているかを確認したり、危険なコードが含まれていないか確認したり、技術を盗んだりすることが容易になります。 機械語コードをコンパイル前のコードに変換することを逆コンパイルといい、特にアセンブリに変換することを逆アセンブリといいます。 Scalaコードをコンパイルして生成されるクラスファイルをJVMコードやJavaコードといった人間が読めるコードに変換することができます。
.class拡張子を持つクラスファイルをjavapコマンド(Windows, Unix)で逆アセンブリをしてJVMコードを調べる方法があります。
javap -c クラスファイル.class
例えば次のような場合に役に立ちます。switch文はJVMコードではtableswitchとlookupswitchのいずれかに変換されます。tableswitchは高速にswitchするためのテーブルが作られるため高速な処理ですが、lookupswitchはテーブルが作成できない場合のswitchなのでtableswitchより遅い処理です。JVMコードでどちらが使用されているかは逆アセンブリすることで確認できます。逆アセンブリした結果、lookupswitchが使用されていてtableswitchを使用したい場合は、Scalaコード上でcase文が連番になるようにダミーのcase文を挿入するか、switch文を分割するといった対策が考えられます。
.class拡張子を持つクラスファイルを逆コンパイラで逆コンパイルしてJavaコードを調べる方法があります。Scalaコードをコンパイルして生成されたクラスファイルであってもJavaコードに逆コンパイルできます。
逆コンパイラのJadでは次のように実行します・
jad -r クラスファイル.class
逆コンパイラは、例えば次のような場合に役に立ちます。
再帰関数は関数の呼び出しの階数が深くなりすぎるとStackOverflowErrorが発生します。再帰関数が末尾再帰で実装されていれば、末尾再帰の呼び出し最適化により再帰関数のStackOverflowErrorの問題を解決できます。末尾再帰とは、再帰呼び出しがその処理の最後に行われるような再帰のことです。
末尾再帰の例(loopメソッドが末尾再帰になっています)
def fibo(n: Int): Int = {
@tailrec
def loop(n: Int, prev: Int, cur: Int): Int = {
(n: @switch) match {
case 0 => prev
case _ => loop(n-1, cur, prev+cur)
}
}
loop(n, 0, 1)
}
この例のように、末尾再帰になっているメソッドには@tailrecアノテーションを付けることで末尾再帰の呼び出し最適化が行われます。
末尾再帰ではない例(fiboメソッドは再帰関数ですが末尾再帰にはなっておりません)
def fibo(n: Int): Int = {
(n: @switch) match {
case 0 => 0
case 1 => 1
case _ => fibo(n-1)+fibo(n-2)
}
}
この例のように、末尾再帰になっていないメソッドに仮に@tailrecアノテーションを付けるとコンパイルエラーが発生してコンパイルできません。
末尾再帰の呼び出し最適化とは、再帰時に呼び出し元へ戻るための戻り先情報をスタック領域に保存しないで再帰させるような機械語コードを出力させることです。 そのようにすることでスタック領域に大量の戻り先情報を保存せずに済むため、再帰関数の呼び出し階数が深くなることで発生するStackOverflowErrorが起こらなくすることができます。実は、最適化する際に実質的には末尾再帰を再帰関数からループに変換しています。 そのため、@tailrecアノテーションが付いた末尾再帰のScalaコードをコンパイルして生成したクラスファイルを逆コンパイルして生成されたJavaコード上では末尾再帰がdo-while文に変換されていることが確認できます。
関連文書:
なぜ1文字が1つのChar型で表せなくなったのか、世界統一に向けた文字コードの歴史を簡単に振り返ってみましょう。
なぜJavaで1文字が1つのchar型で表せなくなったのか、文字コードの歴史を振り返るにあたって、歴史を想像しやすいようにランドマークとして一般的な近現代史を足しました。 太字の箇所 だけ読んでいただければ十分です。
最初に文字をコード化・電子化したのは1800年台半ばのモールス信号です。
今日文字入力装置として使われるキーボードの原型は、1900年代初めのタイプライター標準化で生まれました。
1945年に第二次世界大戦が終結します。宮城事件。
1946年に最初のコンピュータの一つと言われるENIAC(エニアック)が登場します。
1949年、映画とその主題歌「青い山脈」発表。
1950年にアメリカでLeroy Andersonが"The Typewriter"という現代オーケストラ曲を作曲します。
1950年代に中国簡体字が登場し、50年から53年の朝鮮戦争で朝鮮半島が北朝鮮と韓国に分裂します。その後、韓国でナショナリズムが高まり、その結果70年に韓国が漢字廃止政策を行います。1956年から日本は高度成長期(実質GDP成長率約9.1%)に入ります。1957年アメリカでElvis Presleyが「監獄ロック(Jailhouse Rock)」を発表。60年代からイギリスのバンドThe Beatlesが音楽業界の多くの慣習を破壊しインド巡礼やLSDを使用した精神解放を宣伝し、それに感化されたアメリカのLSDで頭がお花畑になったヒッピーたちがベトナム戦争の反戦運動とニューエイジ運動を起こし、そのヒッピーの反戦運動に感化されたのか高度成長期のおかげでお金や就職先に困らない日本の暇な若者の一部が日本では学生運動を起こしオルグ活動により広まっていきます。東大安田講堂事件で学生運動家のバカに火がつきます。70年代に国鉄労働組合によるストライキが頻発。ここら辺からは日本では第二次世界大戦終戦の数年後に生まれた第一次ベビーブーム(団塊の世代)の大勢の子どもやその先輩・後輩たちの中の一部の学生運動家とその親の世代の国鉄労働組合の一部が幼稚な言論に基づき過激な行動に出て社会に大迷惑をかけ、その背後では北朝鮮が日本人を拉致していたというとても特に嘆かわしい時代です。
1970年に毛沢東の文化大革命が終わり、日本では70年三島事件、72年連合赤軍による山岳ベース事件、あさま山荘事件や日本赤軍によるテルアビブ空港乱射事件などが勃発し、73年は2月の固定相場制から変動相場制への以降と10月の第一次オイルショックで日本は狂乱物価になり日本の高度成長期が終わり安定成長期(平均実質GDP成長率約4.2%)に移り、ベトナム戦争が終結、米軍の撤退と同時に75年頃から4年間カンボジアでポル・ポト政権が毛沢東主義をベースにした原始共産主義を掲げ、自国民の主に知識人を大量に虐殺します。この時のポル・ポト政権による大虐殺が、後にUnicodeへのクメール語の文字登録に悪影響を与えます。77年日本赤軍が引き起こしたバングラデシュ・ダッカでの日航機ハイジャック事件での犯人側の釈放要求などに対して福田赳夫首相が「人の命は地球より重い」と発言し、これに応じました。この前後10年ほどの間に北朝鮮による日本人拉致事件が行われていました。この頃、日本の学生運動は一連の内ゲバ事件やテロ事件により冷めていきます。1978年に黒人のディスコバンドEarth Wind & Fireの"September"発売しました。
1979年に社会学者エズラ・ヴォーゲルによる"Japan as Number One: Lessons for America"が出版され、終身雇用制や年功序列賃金などの日本的経営が世界的に評価されます。この年のイラン革命が第二次オイルショックを引き起こします。
1983年、大韓航空機撃墜事件。
1984年、欧州電子計算機工業会(ECMA)と米国国家規格協会(ANSI)がECMA-94という今日Latin-1と呼ばれるラテン文字の文字コードの原型が制定されます。同年、ISOの文字コード規格委員会が16bit固定長のISO 10646を構想します。
84年、Culture Clubの"Karma Chameleon"、ディスコ曲のDead or Aliveの"You Spin Me Round"発売。
1985年、Michael Jacksonがアフリカの飢饉救済プロジェクトUSA for Africaのチャリティーソング「We Are The World」を発表しました。
プラザ合意により円安状態から急激に円高方向に振れ2〜3年で240円/USD付近から120円/USD代まで進みました。尾崎豊「卒業」発売。
1987年、XeroxのJoe BeckerとLee Collinsが世界中の文字を16bit固定長で表すUnicodeを構想します。 国鉄分割民営化。
***1989年、16bit固定長のUnicode Draft 1を発表します。***同年、中国は鄧小平の時代、天安門事件が勃発します。ベルリンの壁崩壊。X Japan (当時はX)の「紅」が発売。12月29日の東証大納会で日経平均株価が史上最高値の38,957円44銭(同日終値38,915円87銭)を記録。 年明けの東証大発会とともにバブルが崩壊し、デフレ不況による失われた20年が始まります。
1990年、ISO 10646の原案では漢字コードを32bitで各国の文字コードをそのまま扱うことになっていました。
中国が漢字を国ごとではなく統合的に扱うことを要請しCJK-JRGを設置しました。よど号ハイジャック事件。
1991年、32bit固定長のDIS 10646第一版は否決され、16bit固定長のUnicodeとの一本化が図られ、Unicode 1.0.0が発表されました。
世界中の文字を16bit、つまり65,536個の容量で管理しようとしましたが、中国や台湾などの漢字が多すぎたためすぐに挫折します。ソ連崩壊により冷戦が終結します。
中国や台湾からの漢字領域への大規模水増し申請が起こりました。
1993年 CJK統合漢字が割り当てられたUnicode 1.1が制定されました。
ハングル文字はチャモの組み合わせが大量に存在しますが、韓国の申請により使用頻度の低いハングル文字もBMP領域に入れることになりハングル大移動が起こりました。Ref. Unicode2.0「ハングル大移動」の経緯 - Togetterまとめ 。中国と台湾による架空の文字集合拡張が行われました。次は、Ken Lunde(Twitter)著・CJKV日中韓越情報処理の「3.2.3 架空の文字集合拡張」からの引用です。
多数の国家規格からISO 10646-1:1993 (Unicode)の漢字20,902字がまとめられた時、それぞれの国家規格開発団体がCJK-JRG(CJK Joint Research Group、現在のIRG - Ideographic Rapporteur Group)に文字集合の資料を提出した。しかしながら、ある特定の漢字集合が20,902字の最終案に盛り込まれることを確実にするために、少なくとも2つの国家規格は架空の拡張を含めて提出された。つまり、規格の一部でもなく、またそのようになる可能性もない漢字が追加されていたのである。これに該当する文字集合は、中国のGB/T12345-90と台湾のCNS 11643-1986の14面(当時CNS 11643-1992がまだ公布されていなかったことを忘れてはならない)である。
GB/T 12345-90の場合、区点89-09より後の符号位置には疑問がある。これらの符号位置は架空の拡張である可能性が大きい。また、CNS 11643-1986の14面の場合は、区点68-21より後の符号位置を疑うべきである。同様にCNS 11643-1992の3面(これはCNS 11643-1986 14面のはじめの6,148字と同じ)では、区点66-38より後の符号位置が疑わしい。
安岡孝一は、これらの架空の拡張を含まない代わりのUnicode対応表を開発した。
こうした中、Java言語の前身であるOak言語がUnicodeを採用し、char型を16bit固定長に設計しました。
1994年 Oak言語がJava言語に名称変更、翌年95年にJava言語が公開されました。
C/C++言語のchar型は8bit固定長で256文字しか扱えないため、日本語処理ではEUC-JPやShift JISなど日本語文字を16bitで表す文字コードを用いて無理やり2つのchar型で1文字を管理していました。そのため、Javaでchar型が16bitに倍化したことの意義は大きいです。
なぜ、32bitの容量がchar型に採用されなかったのかの理由については当時どのような議論があったのか調べる必要があるが、この点について私はまだ調べられておりません。
考えられることとしては、
ラテン文字の少ない文字数で事足りる西欧人からすると8bitから16bitでメモリの使用領域が倍化するのに、32bitになれば4倍にもなり、そのほとんどが西欧人にとって必要のないものであるため基本的に容量が増えることに反対であったと考えられます。さらに漢字統合が起これば登録すべき字数が少なくなること、使用頻度により容量を変化させる折衷案が存在することなどから、折衷案に収束していったと考えられます。この結果、C/C+言語で日本語文字を8bit型2つで表していた問題は、Javaでは日本語文字の大部分は16bitのchar型1つで表せるが、一部はchar型を2つ組み合わせて表す方式になってしまいました。
1995年 阪神・淡路大震災。ニューエイジ運動の影響を受けたであろうオウム真理教が地下鉄サリン事件を起こしました。
1996年、Unicode 2.0.0が発表され、容量を16bitから21bitに拡張、未登録漢字を追加領域に登録し、16bit2つで追加領域を表現するサロゲートペアが登場します。
1999年、Unicode 3.0.0が発表され、ベトナムの文字欄が追加され、カンボジアのクメール文字を追加される。しかし、ポル・ポト政権により知識人が大量虐殺されたことに影響し、クメール文字に存在しない文字が登録されたり、存在する文字が登録されていなかったり、登録順序が不整合な登録でした。Ref. クメール文字とUnicode - Togetterまとめ, 「クメール文字とUnicode」補遺 - Togetterまとめ
同年、北朝鮮が「金正日」と「金日成」を表すハングルの追加を申請しました。Ref. KPS 9566-2000
GLAYの20万人動員ライブ(GLAY EXPO '99 SURVIVAL)。
2000年、ITバブル崩壊、ITバブル崩壊やサブプライム住宅ローン問題などが08年にリーマン・ショックを引き起こす原因となり、サブプライム住宅ローン危機、世界的な金融緩和競争を呼び、11年に「ウォール街を占拠せよ」の原因の一つになりました。
2001年、アメリカ同時多発テロ発生。
2003年、Javaのchar型をCharとして継承したScala言語が公開されました。
Javaの変数は参照を変えられますが、この表の例のようにScalaでval
を使用すると参照を変えることができません。もしJavaと同じように参照を変えたい場合は、val
からvar
に変更してください。
Unicodeが誕生して今日のように普及する前は、日本語テキストはShift JISやEUC-JPといった符号化方式が主に使用されていました。Shift JISやEUC-JPの一部はJIS X 0201であり、ASCIIのバックスラッシュ記号である0x5Cのコードポイントに、JIS X 0201では日本語の半角円記号が登録されています。このため見た目が半角円記号であってもバックスラッシュ記号のコードポイント 0x5Cを表していることがあります。スライドでは半角円記号で表示しましたが、0x5Cの意味で半角円記号を表記しました(時間があったら直します)。そのような背景からUnicodeではU+005Cはバックスラッシュ記号とし、U+00A5に半角の円記号が設置されました(全角円記号はU+FFE5)。
Javaでは、==演算子(!=演算子)はプリミティブ型の一致(不一致)は見られてもString型やプリミティブラッパークラスのような参照型に対してはスタック領域上にある参照の一致を見てしまい内容の一致を見ることはできないため、仮に内容が一致していてもfalseを返す可能性があります。そのため、参照型ではequalsメソッドにより内容の一致を見なければなりません。これはあまり直感的ではなくしばしばバグを生む原因になります。一方で、Scalaではプリミティブ型は存在せず、全てが参照型であり、全ての参照型に対して==演算子(!=演算子)とequalsメソッド(!演算子を伴うequalsメソッド)で内容の一致を見ることができます。Javaのように参照の一致を見たい場合は、eqメソッド(neメソッド)が使用できます。
Javaでは、String型はequalsメソッドを用いなければ内容の一致を見ることができませんが、char型はプリミティブ型なので==演算子で一致を見ることができます。さらに、char型は整数型(16bit)なので、他の整数型(byte型は8bit、short型は16bit、int型は32bit、long型は64bit)や浮動小数(float型は32bit、double型は64bit)と==演算子(!=演算子)で値の一致や比較を見ることができます。ScalaでもChar型は整数型なので同様に数値としての一致・比較が可能です。