Java17黒本2章覚え書き

2024/12/11に公開

はじめに

Java17黒本の2章を読みながら覚え書きとして、重要なポイントをまとめています。
※ 一部参考にした記述はありますが、基本的には自分の言葉におきかえや要約したり、サンプルコードも分かりやすさ重視で記載しています

TL;DR

  • Javaのデータ型や変数の宣言方法が一目でわかる
  • Stringの便利メソッドの使い方まとめ
  • 「StringBuilder」を使った動的な文字列の操作方法がわかる
  • 配列の宣言方法やそれぞれの特徴や操作方法まとめ

覚書

Javaのデータ型

Javaで扱えるデータ型は主に以下の通り

  • プリミティブ型
    • boolean
    • char
    • byte
    • short
    • int
    • long
    • float
    • double
  • 参照型
    • オブジェクト型
      • クラスで定義されるやつ
    • 列挙型
      • Enumで定義されるやつ
    • 配列型
      • ArrayListやListで作成できる

リテラル

リテラルとは、ソースコード中に記述する値のこと

リテラルの種類

Javaには以下のリテラルが存在する

  • 整数
  • 浮動小数点
  • 真偽
  • 文字
  • null

リテラルのデータ型の明示

  • long型:100Lや2l
  • float型:1.0Fや2.1f
  • 2進数(0bが接頭辞):0b101
  • 8進数(0が接頭辞):012
  • 16進数(0xが接頭辞):0x2A

1.0はデフォルトでdouble型、10はデフォルトでint型になるので
float型やlong型を利用したい場合は明示が必要

桁の明示

数値リテラルには「_」で桁を明示することができる

1000000000000000 -> 1_000_000_000_000_000

以下のルールに従う必要あり

  • リテラルの先頭の末尾には記述できない
    • _1000_0001_000_000_はNG
  • 記号の前後には記述できない
    • 0x_12_34123_.000はNG

識別子の命名ルール

変数やメソッド、クラスの名前を「識別子」と呼ぶ

以下の通り命名のルールが存在する

  • Javaの予約語と重複した命名はNG
    • forやintなどはNG
  • 英数字と記号($と_)
    • $は見たことないな・・・
    • _は予約後を使いたいとき利用するかも
      • var _list = ["hoge"]みたいな
  • 数字から始まる命名はNG
    • 10LoopはNG

Javaの命名についての参考サイト
https://qiita.com/rkonno/items/1b30daf83854fecbb814

型推論

Javaではvarを使うことで、ローカル変数定義時に代入する値を見て型を推論できる

var keyword1 = "寿司" // keywordはStringと推論される
var keyword2 = 39 // intと推論される

ただし、推論できない場合がある

  • ラムダ式
    • var a = () -> {}
  • 配列
    • var a = {1,2,3}
  • クラスフィールドの宣言
public class Keyword {
  String name = ""; // OK
  var config = ""; // NG
}

Stringによる文字列の定義

Javaでは文字列はStringクラスで定義できます

定義方法はいくつかあります

  • ダブルクオーテーションを利用
    • String text = "寿司"
  • newでStringのインスタンス生成
    • String text = new String("寿司")
  • valueOfメソッドの利用
    • String text = String.valueOf("寿司")

注意点

文字列はダブルクォーテーションで定義する。シングルクォーテーションではエラーになる

  • 文字列はダブルクォーテーション
    • String text = "寿司"
  • 文字はシングルクォーテーション
    • char text = 's'

イミュータブルとミュータブル

Javaでは、内部のデータを変更できるオブジェクトのことを「ミュータブルなオブジェクト」
変更できないオブジェクトのことを「イミュータブルなオブジェクト」と呼びます

イミュータブルなオブジェクトを定義するためには?

  • すべてのフィールドをprivateで修飾
  • 内部のデータを変更できないようにする(setterを作らない、フィールドはfinalで宣言)
  • クラスをfinalで宣言し上書きを禁止する

代表的なイミュータブルなオブジェクト

StringFileは代表的なイミュータブルなオブジェクト

Stringは上書きできないので、文字列を変更する場合は新しいインスタンスの生成が必要

Stringの便利メソッド

文字列から指定の位置の文字を抜き出す

charAtメソッドで文字列から指定の位置の文字を抜き出しできる

"world".charAt(1)
// 'w'が出力

文字列から指定した文字の位置を返す

indexOfメソッドで文字列から指定した文字の位置がわかる

"world".indexOf('w') // 0が出力
"world".indexOf("or") // 1が出力
"world".indexOf("a") // 存在しないので、-1が出力

文字列から指定した範囲の文字列を返す

substringメソッドで文字列から指定した範囲の文字列を取得できる

"world".substring(0, 2) // woが出力
"world".substring(2, 3) // rlが出力
"world".substring(4, 6) // 範囲外のためエラー

文字列から指定した文字列を置き換える

replacereplaceAllメソッドで文字列から指定した文字列を置き換えできる

"world".replace('w', "h") // 0が出力
"world".replaceAll("w", "h") // 1が出力

replaceAllだと正規表現が使える

"aaaaa".replaceAll("[a-z]", "l") // 11111が出力

文字列長のカウント

lengthメソッドで文字列の長さを取得できる

"two".length() // 3が出力
"world".length() // 5が出力

文字列が指定した文字で始まるか判定

startWithメソッドで文字列が指定の文字列で始まるか判定できる

"two".startWith("tw") // trueが出力
"world".startWith("tw") // falseが出力

文字列が指定した文字で終わるか判定

endWithメソッドで文字列が指定の文字列で始まるか判定できる

"two".endWith("wo") // trueが出力
"world".endWith("wo") // falseが出力

文字列の結合

concatメソッドで文字列と文字列を結合できる

"hello".concat("world") // "hello world"が出力

文字列の比較(==やequals)

Stringには「コンスタントプール」という仕組みがあり、プログラムの中に出現する文字列を定数値としてインスタンスとはことなる定数用のメモリ空間に保存される

そのため、以下のように定義した文字列は同じメモリへ参照されます

String a = "hoge";
String b = "hoge";
// aとbは同じメモリを参照するため
// a == b は trueになる

一方で、以下の場合は新しいインスタンスが作成

String a = "hoge";
String b = new String("hoge");
// aはコンスタントプールを参照、bはインスタンス用のメモリ空間を参照
// a == b は falseになる

上記のような例があるため、一般的に文字列比較は equalsメソッドを使ったほうが安全といわれている

String a = "hoge";
String a = new String("hoge");
System.out.println(a == b); // falseが出力される
System.out.println(a.equals(b)); // trueが出力される

internメソッドを使う方法もある。
このメソッドはコンスタントプールを含むメモリ内の文字列を探して参照先を返すことで、newしても同一の文字列か比較できる

String a = new String("hoge");
String b = new String("hoge");
System.out.println(a.intern() == b.intern()); // trueが出力される

StringBuilder

StringBuilderは、文字列を効率的に操作するためのクラス
普通の文字列操作(String)はイミュータブルで、操作ごとに新しい文字列が作られるため、StringBuilderを使うとパフォーマンスが向上する

注意点

StringBuilderでは、デフォルトで扱える16文字分のバッファ+指定した文字列の長さのため、それ以上の文字列を操作できない。その場合は、以下のように容量を指定したインスタンス生成が必要になる

StringBuilder sb = new StringBuilder(50);

1. インスタンスの作成

StringBuilder sb = new StringBuilder("Hello");

2. 文字列を追加

appendメソッドで文字列を末尾に追加

sb.append(" World"); // Hello Worldになる

3. 文字列を挿入

insertメソッドで指定した位置に文字列を挿入

sb.insert(5, " Java"); // Hello Java Worldになる

4. 文字列を置換

replaceメソッドで指定した範囲を置換

sb.replace(6, 10, "Cool"); // Hello Cool Worldになる

5. 文字列を削除

replaceメソッドで指定した範囲を削除する

sb.delete(5, 10); // Hello Worldになる

6. 文字列を逆順

reverseメソッドで文字列を逆順にする

sb.reverse(); // dlroW olleHになる

7. 指定した文字列の位置を取得

indexOfメソッドで指定した文字列の開始位置を取得する

sb.indexOf("l"); // 1になる

8. 現在の文字列を取得

toStringメソッドでStringとして文字列を取得する

sb.toString();

テキストブロック

Java SE15からテキストブロックが使えるようになった
トリプルクオーテーション """で文字列を囲むことで複数行にまたがる文字列を扱いやすくなった

ユースケースとして、HTMLや何かしらのクエリを文字列として扱うことが考えられる。既存と比べて可読性や保守性の向上が見込まれそうです。

入力例

  • "Hello, World"のダブルクオーテーションや改行\nもエスケープが不要になった
  • テキストブロックのインデントは、もっともインデントが少ない行を基準に自動で調整される
    • 例えば、以下の場合、もっともインデントが少ないのは<html>なので、前に入るスペースは削除される
String html = """
    <html>
        <body>
            <h1>"Hello, World!"</h1>
        </body>
    </html>
    """;

System.out.println(html);

出力例

不要なスペースは削除されている

<html>
    <body>
        <h1>"Hello, World!"</h1>
    </body>
</html>

注意点

トリプルクォーテーションのあとは改行が必要

// これはNG
var text = """hoge
fuga bar
"""
------

// これはOK
var text = """
hoge fuga
bar""" // barはトリプルクォーテーションのあとなのでOK

配列の宣言

1次元配列の宣言

int[] array; // arrayはint型の配列として宣言
int array[]; // 他の言語のように宣言もできる

多次元配列の宣言

int[][] array; // arrayはint型の2次元配列として宣言
int array[][][]; // 他の言語のように宣言もできる
int[] array[]; // 2次元配列として、このようにも宣言できる

配列インスタンスの生成

int[] array = new int[3]; // 要素数3の1次元配列のインスタンスを生成
int[] array = new int[]; // 要素数を指定しないとエラーになる

要素数は必ず整数

double[] array = new double[5.5]; // エラーになる。要素数は必ず整数を指定する
double[] array = new double[5]; // これはOK。要素数5の配列を作成
array[0] = 1.1;
array[1] = 2.2;
array[2] = 3.3;
array[3] = 4.4;
array[4] = 5.5;

配列宣言と初期化

初期化子 {}を使って、配列の宣言と参照の代入、配列インスタンスの生成、初期化を1行でいっぺんにできる

int[] array = {1, 2, 3}; // 要素数3のint型配列のインスタンス生成
int[] array = new int[]{1, 2, 3}; // この書き方もできる。ただしnew int[]に要素数は書かない

多次元配列も同様に宣言できる

int[][] array = {{1, 2}, {3}}; // 2次元配列のインスタンス生成
int[][] array = new int[][]{{1, 2}, {3}}; // この書き方もできる。ただしnew int[][]に要素数は書かない

配列の複製(クローン)

cloneメソッドを使うことで配列を複製できる。
配列のインスタンスはそれぞれ異なるが、要素毎の参照先はarray1とarray2は同じ

int[] array1 = {1, 2, 3};
int[] array2 = array1.clone(); // array1と異なるインスタンスのarray2を生成
System.out.println(array2[0]); // 1が出力。 参照先はarray1[0]と同じ

動的配列

ArrayListを使うことで要素数を固定せず動的な配列が利用できる

ArrayList list = new ArrayList<>();
list.add(100); // 要素を追加
list.add("text"); // 型を指定していないので文字列も追加できる

for (Object obj : list) {
    System.out.println(obj);
}

<>はダイヤモンド演算子で型を指定できる。ただし、指定できる型は参照型のみ許容される
※ プリミティブ型は指定できない

ArrayList<int> list = new ArrayList<>(); // プリミティブ型は指定できないからNG
ArrayList<Integer> list = new ArrayList<>(); // ラッパークラスはOK
list.add(1); // 要素を追加
list.add(2);

for (Integer num : list) {
    System.out.println(num);
}

ArrayListの便利メソッド

要素を追加

addメソッドで要素を追加できる

ArrayList<Integer> list = new ArrayList<>(); // ラッパークラスはOK
list.add(1); // 要素が追加され配列は[1]になる
list.add(2); // 要素が追加され配列は[1, 2]になる

位置を指定して要素を追加できる

list.add(1, 3); // 要素が追加され配列は[1, 3, 2]になる

要素を置き換え

setメソッドで要素を置き換えできる
第一引数は置き換えしたいインデックス、第二引数は置き換えたい値を記載する

ArrayList<Integer> list = new ArrayList<>(); // ラッパークラスはOK
list.add(1); // 要素が追加され配列は[1]になる
list.add(2); // 要素が追加され配列は[1, 2]になる
list.set(1, 3); // 要素が置き換わり配列は[1, 3]になる

要素の削除

ArrayList<Integer> list = new ArrayList<>(); // ラッパークラスはOK
list.add(1); // 要素が追加され配列は[1]になる
list.add(2); // 要素が追加され配列は[1, 2]になる
list.add(3); // 要素が追加され配列は[1, 2, 3]になる
list.remove(1); // 要素番号1の値2が削除され配列は[1, 3]になる

固定長のリスト

ArraysクラスのasListメソッドとListインタフェースのofメソッドで固定長のリストを生成できる。

var list1 = List.of(1,2,3); // [1, 2, 3]が生成される
list1.add(4); // エラーになる。List1は固定長なので要素追加できない
var list2 = Array.asList(new Integer[] {1, 2, 3}); // [1, 2, 3]が生成される
list2.add(4); // エラーになる。List2は固定長なので要素追加できない

Listはイミュータブルなリストを作れるので、個人的にはこれを推奨している。
大規模な開発だと、どこかのタイミングで値が変わる場合、予期せぬ動作につながる恐れがある。
イミュータブルな値を扱うことで、その懸念がなくなり安全にコーディングできることはメリットになる。

これまで紹介した配列の特徴の比較表

これまで紹介した配列との違いを表にまとめると以下の通り

配列(int[] ArrayList List.of Arrays.asList
サイズ 固定 動的 固定(イミュータブル) 固定(ただし参照は可能)
変更可能性 不可(イミュータブル) 一部可(元の参照次第)
速度 高速 やや遅い 高速(生成後変更しないため) 高速(生成後変更少ない場合)
機能 少ない 豊富 少ない(読み取り専用) 限定的(サイズ変更は不可)
柔軟性 低い 高い 中程度(読み取り専用に限る) 中程度(固定サイズのみ)
使い方 基本的な配列操作 便利なメソッドが使える 読み取り専用リスト作成に最適 テストや簡易的な操作に便利
適用範囲 サイズが固定の場合 動的なリストが必要な場合 固定データを扱う場合 元の配列を操作しない場合

各選択肢の概要

配列(int[]

固定サイズのデータ構造で、サイズが決まっている場合に使用。

int[] array = {1, 2, 3};

ArrayList

動的にサイズが変えられるリスト。追加や削除が必要な場合に最適。

ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);

List.of

Java SE9以降で追加された、イミュータブル(変更不可)のリストを作成するメソッド。データが変更されない場合に最適。

List<Integer> list = List.of(1, 2, 3);
// list.add(4); // 実行時エラー(UnsupportedOperationException)

Arrays.asList

既存の配列からリストを作成する静的メソッド。簡易的なリスト操作に便利。

List<Integer> list = Arrays.asList(1, 2, 3);
list.set(0, 10); // OK // 参照先を変えるのはOK
// list.add(4); // 要素長固定なので追加するとエラー(UnsupportedOperationException)

選択肢のポイント

  • 配列(int[]): 高速・シンプルだが、サイズ固定で機能が少ない
  • ArrayList: 汎用的な動的リスト。追加・削除が必要な場合に最適
  • List.of: イミュータブルなリスト作成が簡単。変更しないデータに最適
  • Arrays.asList: テストや一時的な操作に便利だが、サイズ変更には対応しない

おわりに

Java11からJava17の黒本の追加要素として以下の機能が印象的でした

  • varによる型推論を使った変数宣言
  • テキストブロックによる複数行またぐ文字列の作成

どれも他の言語では一般的にある機能なので、それがJavaで使えるようになりより便利になったと感じました。

以上です。他の章も余力があれば覚え書き作成したいと思います。

株式会社ZOZO

Discussion