Javaまとめ
Javaの特徴
- 1996年にサン・マイクロシステムズによって開発された
- 2010年にオラクルに吸収合併されたことによって版権もそちらに移っている
- クラスベースのオブジェクト指向言語
- マルチスレッド
- ガベージコレクション
- JVMという仮想マシン上で実行されるため、実行環境を問わないプラットフォーム非依存な言語
- webアプリケーション、API、モバイル、組み込みといったさまざまな開発に使用されている
- 世界中で使用されている言語だが、特に日本では金融システムのような大規模で堅牢性の問われるシステムに広く使われている
- 2023年9月にJava21がリリース
- Java はレガシーと言われることが多いがJava8では関数型インターフェースとラムダ式、ストリームAPIが導入されており、最近のJavaではよく使用されているモダンな言語にあるような様々な機能も導入されておりJava自体は進化を続けている
- Javaがレガシーなのではなく、Javaが使われている環境がレガシーなことが多い
- レガシーという言葉も悪い意味ではなく、IT業界で「枯れている」という言葉は安定して使われてきたということでもあり、公開されている情報も多いため、悪いことではない
- 実際の開発現場では最新の技術よりも充分に枯れている技術が採用されることも多い
- Javaは静的型付け言語のためプログラムを実行する前にコンパイルを必要とし、コンパイル時に型を必要とする
- 動的型付け言語は実行時に書いたプログラムを解釈するインタプリンタ方式
- Rubyなどでは実行時にコンパイルしながら実行するJITコンパイラなどを採用していたりする
- なので動的言語だから遅いという浅い考えは捨てた方がいいし、静的型付言語の方が型があって速いみたいな考えもやめたほうがいい
- Javaはコンパイルによってプログラムをバイトコードに変換する
- Javaの言語設計の核心であるWrite Once, Run Anywhereは一度書いたらどこでも実行できるとあるように、JVMという仮装マシン上で中間言語を実行するようにしたことで実現されている
インタプリンタとコンパイル
Javaのようなコンパイル言語と比較される動的言語はコンパイルを必要としないでインタプリンタによって解釈される。このインタプリンタにより解釈の部分がいまいち具体的に何をしているのか気になったので調べたけどあんまりいい答えは得られなかった。
コンパイル言語が中間言語や機械語にコードを変換して実行するというのはいいのだが動的言語がコードを解釈して実行とは何なのか??
これは書いてあるそのままでコードをインタプリンタが解釈して実行するという説明以上にいい説明はなさそう。解釈というのは中間言語や機械語に変換、つまりコードを実行時に解釈してコンパイルし、実行していると思っていたがそれは違うらしい。そういうインタプリンタを採用した言語もあるが、機械語や中間言語に変換するのは絶対ではないらしい。インタプリンタの実行速度を改善するためにJITコンパイラが組み込まれていたりするが、JITコンパイラが逐次的に必要な箇所をいい感じにコンパイルしてキャッシュして実行するような方式なのでインタプリンタ方式がコードを実行時にコンパイルするという風に誤解されるようになったのかもしれない。それをやってるのはJITコンパイラでありインタプリンタではない
コメント荒れてるけど参考
Javaのコンパイルの仕組みは?
Javaはjavacコマンドを使い.java
ファイルから.class
ファイルにコンパイルする。これはJavaのソースコードをJVMが解釈できる中間言語(バイトコード)に変換している。実行はJVM上で実行され、バイトコードをインタプリンタもしくはJITコンパイラが読み取り実行される。一言でコンパイルといってもこのように思っているよりも複雑。
JDKとJRE
JDKはJava Development Kit。Javaで開発するためのツール一式が揃っている。
JREはJava Runtime Environment。これはJavaの実行環境。
JDKはコンパイラであるjavacも実行環境であるJREも含んでる。今からJavaで開発しようとするならJDKのことだけ考えていればよさそう。
Javaのディストリビューション
以下の記事がわかりやすい(岸田さんの記事なので内容の信憑性は問題ないと思う)
2017年にバージョンアップの方針の変更と同時にJavaを有償化する話が出た。これはOracle社のサイトからダウンロードしたJavaを使ってwebサービスを運用する場合に有償になるのはなるが、それ以外の無償のJavaを使うことは普通にできる。
それ以外の無償のJavaというのはAmazonのcorrettoやTemrinなど複数の無償ディストリビューションがある。
Javaの実装としてOSSで公開されているのはOpenJDKのみでこれをビルドして実行ファイルとして配布しているのが各ディストリビューションということになる。
JavaとOracleの話はしばしば話題に上がる(主にライセンスの話)がOracle以外の無償Javaを使うのであればそこまで関係ない、はず。
とりあえず、Javaの学習やJavaで開発するだけであればOracleやライセンスの話は関係ない
Javaのリリースサイクル
Java17まではあんまり定まっていない感があるがJava11からJava17の間は3年だった。しかし、このリリースサイクルがうまくいっていたのとJavaの新仕様を使ってもらうためにLTSのサイクルが2年になったようで2023年の9月にJava21がリリースされている。これが最新のLTS。次は 2025年の9月でJava25の予定。
LTSは2年に1回だけどfeatureリリースは年に2回で9月と3月
Javaの環境構築
WindowsとMacの両方を考慮するのめんどくさいからVSCodeのDevcontainersでいい感じにJavaの開発環境を配布できないだろうか??
Docker Desctopインストールして起動するまでがハードル高いかな??
一応VSCodeでJavaを書きたい人はいるみたい
devcontainerの中でgradle initしたところ作成された雛形とgradleのバージョンの相違的なのでビルドもできずここらへんよくいじくること多いけどこんなの未経験の段階でやったら心折れまくるだろうなと思ったのでDevcontainerで開発するのは諦める
素直にIntelliJで
IntelliJは単体でインストールも可能だがToolbox Appを使用してインストールすることで自動アップデートができるらしい。
Javaのインストールは
- IntelliJからインストールする方法
- 自分で用意する方法
があるけどIDEからやると知らない間にJavaがインストールされていてインストールしたという自覚さえなくなるので自分で用意してみる方がいいかも
というわけでSDKMAN
(関係ないけどasdfがあんまり機能していないなぁ)
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk version
SDKMAN!
script: 5.18.2
native: 0.4.6
Windowsは別途インストール方法を確認
# 最新LTSのJavaをインストール
sdk install java
java --version
openjdk 21.0.3 2024-04-16 LTS
OpenJDK Runtime Environment Temurin-21.0.3+9 (build 21.0.3+9-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.3+9 (build 21.0.3+9-LTS, mixed mode)
パスも通ってる
ビルドシステム
MavenかGradleが必要と思いきやIntelliJは内部に独自のビルドシステムを持っているのでGradleすら自分で用意する必要がなさそうだ。
IntelliJのビルドシステムでプロジェクト作成すると拡張子.iml
のファイルが作成されている。さすがにGradleでビルドしたいのでビルドシステムにはGradleを指定。
gradlewを用いてプロジェクトを作成するのでGradleのインストールはローカルマシンに必要ないのか。
ただ、IntelliJでJava21のプロジェクトを作成しようとするとサポート対象外と警告が出る。
IntelliJが古いのか。IntelliJアップデートしたら作成できた。
ローカルにJavaもGradleもインストール不要でプロジェクト作成できるのか。IntelliJ神だな。
build.gradleがgroovyじゃなくてKotlin DSLがデフォルトに変わってそう。
今からだとどっちでやるのがいいんだろうなぁ。。
ディレクトリ構成
.
├── build
│ ├── classes
│ │ └── java
│ │ └── main
│ │ └── org
│ │ └── example
│ │ └── Main.class
│ ├── generated
│ │ └── sources
│ │ ├── annotationProcessor
│ │ │ └── java
│ │ │ └── main
│ │ └── headers
│ │ └── java
│ │ └── main
│ └── tmp
│ └── compileJava
│ └── previous-compilation-data.bin
├── build.gradle.kts
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
├── main
│ ├── java
│ │ └── org
│ │ └── example
│ │ └── Main.java
│ └── resources
└── test
├── java
└── resources
- build ビルドした成果物。バイトコード
- build.gradle.kts ビルドファイル
- gradle gradlew関連のファイル
- src ソース
- .gitignore
- gradlew gradlewの起動スクリプト
- gradlew.bat gradlewの起動スクリプト
- settings.gradle.kts プロジェクト構成の設定など。マルチプロジェクト構成のときなど
gradlew
gradlewとはGradle Wrapperの略称でプロジェクトに組み込まれる自己完結型バージョンのこと。これのおかげでチームメンバーは自分でローカルマシンにGradleをインストールする必要はなく、バージョンを統一してプロジェクトをビルドすることができる。gradle-wrapper.jar
はgradlewの実行に必要なJavaライブラリ。gradle-wrapper.properties
には使用するGradleのバージョンやダウンロードURLなどが記述されている。gradlewの起動スクリプトはgradlew
とgradlew.bat
。
build.gradle(.kts)
ビルドに必要なプロジェクトの依存関係やプラグイン、ビルドタスクなどを記述する。build.gradleは大きく以下のように分けられる。
- プラグイン
- リポジトリ
- 依存関係
- タスク
プラグイン
plugins {
id("java")
}
上記例ではjavaプラグインを使うことを宣言している。
リポジトリ
repositories {
mavenCentral()
}
ライブラリをどこから取得するか。上記例ではMaven Central。jcenterなんかもあったが非推奨になった気がする。
依存関係
プロジェクトの依存関係。
dependencies {
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
}
上記例ではJUnitの依存関係を宣言している。testImplementationでは本番ビルドには含まれないのでテストライブラリなどはtestImplementationで宣言する。
platformはgradle5.0以降で導入され、特定のライブラリ群の依存関係を一元管理するためのもの。上記例ではJUnitのBOMを指定することで、junit-jupiterのバージョンを宣言することなく依存関係を宣言している。
タスク
タスクは独自に宣言して使うこともできる。使用できるタスクは以下のコマンドで確認できる。
./gradlew tasks
以下の例ではtestタスクにJUnitを使用することを宣言している。
tasks.test {
useJUnitPlatform()
}
独自のタスクを定義するには以下のように登録する
tasks.register("hello") {
group = "Other"
doLast {
println("Hello World!!")
}
}
./gradlew hello
> Task :hello
Hello World!!
Javaの基礎文法
- まずはクラスについて
- Javaは一つのファイルに一つのクラスを定義する
- クラスについて詳しくは後述
- とりあえずJavaで何かプログラムを書きたければクラスを宣言してそこに書く必要があるということ
- Javaのプログラムを実行する場合、エントリーポイントを探して実行する
public class Main {
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
- 上記が最小のJavaの実行コード
-
public static void main(String[] args)
がエントリーポイントとなる - これが意味することはクラスの回で詳しく説明する
-
System.out.println()
についてはJavaで標準出力に出力するための記法 - とりあえず何かターミナルに表示させたければこれを使うでおk
型
- Javaは静的型付け言語のため変数の宣言、関数の引数や戻り値に型をつける必要がある
- Javaの型には以下のようなものがある
プリミティブ型
- byte 8ビット整数
- short 16ビット整数
- int 32ビット整数
- long 64ビット整数
- float 32ビット浮動小数
- double 64ビット浮動小数
- char 16ビットUnicode文字
- boolean true or false
ラッパー型
- Byte
- Short
- Integer
- Long
- Float
- Double
- Character
- Boolean
- プリミティブ型を参照型として扱えるようにしたラッパークラス
- プリミティブ型ではnullが扱えないがラッパークラスであれば扱える
- ラッパークラスには便利なstaticメソッドが用意されていたりする
参照型
- クラス 全てのJavaクラス。ラッパークラスも含めて
- インターフェース Listのようなインターフェースも参照型
- 配列 int[]やString[]のような
- 列挙型(enum) enumについて詳しくは後述
文字列
- Javaでの文字列の基本的な宣言は以下
String str = "Java";
- 文字はダブルクォーテーションで囲う
- 文字列の結合
System.out.println("Hello, " + "World!!");
System.out.println("Hello, ".concat("World!!"));
- 文字列の結合はいろいろ方法があるが単純な結合であれば+演算子で良い
- concatのような関数も用意されているが単純な結合であれば対してパフォーマンスに差はないので+演算子で十分
- 基本的に文字列はイミュータブルで文字列を結合するには文字列の再生成が生じるためコストが高い
- なので、文字列の結合頻度が高い場合はStringBuilderなどを使った方がパフォーマンスが高い
// StringBuilder
var sb = new StringBuilder();
for(var i = 0; i < 100; i++) {
sb.append(i);
}
var str = sb.toString();
System.out.println(str);
- StringBuilderは内部的にバッファを持っている
- デフォルトでは16文字分の容量
- Javaは文字列をUTF-16で扱い、UTF-16は一文字2バイトなのでデフォルトでは32バイト
- StringBuilderはバッファを超えると文字列の倍のバッファサイズに自動で拡張する
var sb = new StringBuilder();
System.out.println(sb.capacity()); // 16
sb.append("a".repeat(17));
System.out.println(sb.capacity()); // 34
- よく使いそうな処理
// 文字列の長さ
System.out.println("abcde".length()); // 5
// 小文字、大文字
System.out.println("upper".toUpperCase());
System.out.println("LOWER".toLowerCase());
// 空白 isEmpty, isBlank, trim
var emptyStr = " ";
System.out.println("emptyStr is empty : " + emptyStr.isEmpty()); // false
System.out.println("emptyStr is blank : " + emptyStr.isBlank()); // true
var includeSpaceStr = " Java ";
System.out.println(includeSpaceStr);
System.out.println(includeSpaceStr.trim());
System.out.println(includeSpaceStr.strip()); // Unicodeの空白文字全体を対象
- isEmptyとisBlankで挙動が違うので注意
- isEmptyは空白を許容しないがisBlankは空白を許容するため空白のみの文字列も空とみなす
- 文字列のフォーマット
var name = "Alice";
var age = 20;
var formated = String.format("user name is %s. age is %d", name, age);
System.out.println(formated);
formated = "";
formated = "user name is %s. age is %d".formatted(name, age);
System.out.println(formated);
- フォーマット指定子の代表的なものは以下
-
%s
文字列 -
%d
整数 -
%f
不動少数 -
%x
16進数
-
System.out.printf("%s\n", "Java");
System.out.printf("%d\n", 21);
System.out.printf("%f\n", 1.11); // 1.110000
System.out.printf("%.2f\n", 1.11); // 1.11
System.out.printf("%x\n", 255); // ff
System.out.printf("%04d\n", 1); // 0001
- 文字列抽出と分割、prefixとsuffixへのマッチング
// substring
var day = "2024/5/8";
System.out.println("today is " + day.substring(7) + " day");
System.out.println("today is " + day.substring(0, 4) + " year");
// split
var arr = day.split("/");
for (var s : arr) {
System.out.println(s);
}
// startWith, endWith
class Id {
static String get(int num) {
return num == 0 ? "01-xxxx" : "xxxx-a";
}
}
System.out.println("start with 01: " + Id.get(0).startsWith("01")); // true
System.out.println("end with a: " + Id.get(1).endsWith("a")); // true
- Stringはcharの集合
- charの配列をStringにしたりStringをcharの配列にしたりできる
- charはシングルクォートで囲う
// charの話
char a = 'a';
for(char c : "Java".toCharArray()) {
System.out.println(c);
}
- 正規表現と置換
-
java.util.regex.Pattern
とjava.util.regex.Matcher
を使う - 正規表現は普通に文字列として書ける
- 基本は
Pattern p = Pattern.compile("<正規表現>");
でPatternオブジェクトを作る -
Mathcer m = p.mathcer("<文字列>");
で指定の文字列に正規表現をマッチさせる -
m.find()
ではbooleanが返り、マッチしていればtrueが返る
var p = Pattern.compile("^Hello, World(!+)?$");
var m1 = p.matcher("Hello, World");
var m2 = p.matcher("Hello, World!");
var m3 = p.matcher("Hello, World!!");
var m4 = p.matcher("Hello, World!!$");
System.out.println("m1: "+m1.find());
System.out.println("m2: "+m2.find());
System.out.println("m3: "+m3.find());
System.out.println("m4: "+m4.find());
var p2 = Pattern.compile("(\\d{3})-(\\d{3})-(\\d{4})");
var m5 = p2.matcher("049-111-2222");
if(m5.find()) {
System.out.println(m5.group());
System.out.println(m5.group(1));
System.out.println(m5.group(2));
System.out.println(m5.group(3));
}
- 正規表現でマッチさせて置換とかもできる
var pass = "ajeiuikoljlx";
var passPattern = Pattern.compile("[a-zA-Z]");
var passMatcher = passPattern.matcher(pass);
System.out.println(passMatcher.replaceAll("x"));
- 文字列とbyte配列について
- 文字列はgetBytes()のような関数でbyte[]に変換できる
- このbyte値はエンコーディングする文字コードで変わってくる
- なのでUTF-8とSJISでエンコードした場合のバイト配列の値は同じ文字でも変わってくる
- 現代では問題ないことも多そうだが明示的に文字コードを指定する癖、もしくはどの文字コードでエンコードしているのかを常に意識する癖をつけておくとここら辺の処理でハマっても慌てずに対処できそう
- ちなみに絵文字のようなマルチバイト文字のバイト配列を取得すると以下のような結果になる
System.out.println(Arrays.toString("😄".getBytes()));
// [-16, -97, -104, -124]
- 上記の絵文字は4バイト文字でJavaのbyte型は-128~127の範囲の値を取るため128以上の値は負の値になるため上記のような結果になる
// テキストブロック Java 15
var json = """
{
"name": "Alice",
"age": 20,
}
""";
System.out.println(json);
// 文字列テンプレート Java 21 プレビュー機能
var lang = "Java";
System.out.println(STR."This code is \{lang}");
- 文字列テンプレートはプレビュー機能で実行するのに
enable-preview
オプションをつける必要があったり(つけても警告でたけど)使えるようにするのに手間なので使いたい人だけ使ってください - Unicode文字それぞれにはコードポイントが割り振られている
- このコードポイントだったり文字コードだったりを扱うのは結構難しいところだと思ってる
数値
// 32ビット整数
int num = 1;
Integer num2 = 2;
// 64ビット整数 数字の後に`L`をつける
long num3 = 3L;
Long num4 = 4L;
// 32ビット浮動小数 数字の後に`f`をつける
float num5 = 0.1f;
Float num6 = 0.1f;
// 64ビット浮動小数
double num7 = 0.1;
Double num8 = 0.1;
// 四則演算
// 足し算
System.out.println(num + num); // 2
System.out.println(num + num2); // 3(後述のアンボクシングが働く)
// 引き算
System.out.println(5 -1); // 4
// 掛け算
System.out.println(2 * 5); // 10
// 割り算
System.out.println(10 / 2); // 5
// System.out.println(10 / 0); // 0徐算 ArithmeticExceptionが発生する
// 暗黙的な型変換
System.out.println(num + num5); // 1.1
// オートボクシングとアンボクシング
var list = new ArrayList<Integer>();
// オートボクシングでintからIntegerに自動変換される
list.add(1);
list.add(2);
// アンボクシングでIntegerからintに自動変換される
int sum = 0;
for(int i : list) {
sum += i;
}
// オートボクシングのパフォーマンス影響
// 毎回Integerインスタンスを生成することになりとてもコストがかかる処理になってしまう
// IntelliJはこのようなとき警告出してくれるので便利
Integer sum2 = 0;
for(var i = 0; i < 1000;i++) {
sum2 += i;
}
// Integerはnullを受け入れるので、以下のようにnullが混入する場合もある
// 以下の場合、nullのラッパークラスをアンボクシングしようとしてNullPointerExceptionが発生する
// list.add(null);
// for(var i : list) {
// i++; // この処理に特に意味はない
// }
// 小数の計算は正確には表現できない
// プログラムにおける小数は近似値でしかなく以下のような誤差が生じてしまう
// お金の計算のような誤差が許されないような計算をする場合はBigDecimalを利用する
System.out.println(1.00 - 9 * 0.10); // 0.09999999999999998
var n1 = new BigDecimal("1.00");
var n2 = new BigDecimal("9");
var n3 = new BigDecimal("0.10");
System.out.println(n1.subtract(n2.multiply(n3))); // 0.10
// 文字列 -> 数値
Integer.parseInt("1");
try {
Integer.parseInt("a");
} catch (Exception e) {
System.out.println(e.getClass()); // class java.lang.NumberFormatException
System.out.println(e.getMessage()); // For input string: "a"
}
制御構文
for
// for文
for(int i = 0; i < 10; i++) {
System.out.print(i); // 0123456789
}
/// 拡張for文
var list = List.of(1, 2, 3);
for(var num : list) {
System.out.print(num); // 123
}
if
var result = calculator.add(1, 2);
if(result == 3) {
System.out.println("result is 3!!");
} else if(result == 5) {
System.out.println("result is 5!!");
} else {
System.out.println("result is other!!");
}
三項演算子
- 三項があれば単項や二項もある
- 単項とは
++
や単純に正負の数を表すときの+
記号など - 二項は
1 + 1
や1 + 1 == 2
のようなやつ - 三項はオペランドが三つということらしい
/// 三項演算子
var message = result == 3 ? "result is 3!!" : "result is " + result;
System.out.println(message);
while
// while
// 123445678910
var counter = 0;
while(counter < 10) {
counter++;
System.out.print(counter);
}
counter = 0;
System.out.println();
// 123445678910
// do-whileは最初に処理ブロックが評価されるので絶対一回は処理が実行される
// どちらでもいいがdo-whileでないといけない場面もほとんどないかつ、
// 条件が最初にきていたほうが読みやすいので前者の書き方推奨
do {
counter++;
System.out.print(counter);
} while (counter < 10);
switch
// switch式 Java12
// switch式は式なので変数に結果を代入できる
// breakがいらない
// Java12以降であればswitch式を推奨
var result3 = calculator.add(1, 2);
switch (result3) {
case 0 -> System.out.println("result 0");
case 1 -> System.out.println("result 1");
default -> System.out.println("result " + result3);
}
var result4 = calculator.add(1,1);
var result5 = switch (result4) {
case 0 -> 0;
case 1, 2 -> result4 * 2;
default -> result4;
};
System.out.println(result5); // 4
配列とList, Set, Map
// 配列
// 初期化1 配列のサイズを指定 この場合はサイズ5でnullが格納される
var strArr = new String[5];
// 初期化2 宣言と同時に初期値を指定
strArr = new String[]{"a", "b", "c"};
var arr = new int[2];
// 配列への書き込み
arr[0] = 1;
arr[1] = 2;
// 配列の読み込み
System.out.printf("arr[0]: %d arr[1]: %d%n", arr[0], arr[1]); // arr[0]: 1 arr[1]: 2
// 配列のサイズを超えた要素にアクセスするとエラー
try{
arr[2] = 3;
} catch(IndexOutOfBoundsException e) {
System.out.println("サイズを超えた要素にアクセスするとエラー");
}
// 配列のループ
for(var i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
for(var num : arr) {
System.out.println(num);
}
// 多次元配列
var matrix = new int[][] {
{1, 2, 3},
{1, 2, 3},
{1, 2, 3},
};
System.out.println("---------------------------------------------------");
// List
// Listはインターフェースで実際の実装クラスがある
// 代表的なのがArrayListでLinkedListといったクラスもある
var arrayList = new ArrayList<Integer>();
var linkedList = new LinkedList<Integer>();
// 追加
arrayList.add(1);
arrayList.add(2);
// 取得
System.out.println(arrayList.get(1)); // 2
// 削除
System.out.println(arrayList); // [1,2]
arrayList.remove(1);
System.out.println(arrayList); // [1]
// contains
if(arrayList.contains(1)) {
System.out.println("arrayList contains `1`");
}
// isEmpty
arrayList.clear();
if (arrayList.isEmpty()) {
System.out.println("arrayList is empty");
}
// immutableList
List<Integer> immutableList = List.of(1, 2, 3);
try {
immutableList.add(4);
}catch (UnsupportedOperationException e) {
System.out.println("イミュータブルListは変更できないよ");
}
// sort
var sortList = new ArrayList<>(List.of(2, 3, 1));
System.out.println(sortList); // [2, 3, 1]
Collections.sort(sortList); // 自然な順序でソート
System.out.println(sortList); // [1, 2, 3]
// 匿名クラスでComparatorインターフェースのcompareメソッドを実装する
sortList.sort(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o2, o1);
}
});
System.out.println(sortList); // [3, 2, 1]
// ラムダ式
sortList.sort((o1, o2) -> Integer.compare(o1, o2));
System.out.println(sortList); // [1, 2, 3]
// メソッド参照
sortList.sort(Comparator.comparingInt(o -> o));
// Set
var set = new HashSet<Integer>();
var linkedHashSet = new LinkedHashSet<Integer>();
var treeSet = new TreeSet<Integer>();
var immutableSet = Set.of(1, 2, 3);
// Map
var map = new HashMap<String, String>();
var linkedHashMap = new LinkedHashMap<String, String>();
var treemap = new TreeMap<String, String>();
var immutableMap = Map.of("key1", 1, "key2", 2);
ArrayListとLinkedList
-
ArrayListは内部的に配列を利用している
-
要素の追加や取得はO(1)で非常に高速
-
一方要素の削除やデータの挿入は要素をシフトさせる必要があるため最悪O(N)の計算量で遅い
-
わかりやすいのだと先頭に要素を追加するなどの操作は遅い
-
LinkedListは要素同士を前後双方向のリンクで参照する
-
要素の挿入、削除はリンクの付け替えで済むので高速に動作する
-
一方、要素の取得は先頭から辿る必要があるため最悪O(N)の計算量で遅い
-
基本的にはArrayListで要件は満たせることが多いはず
-
要素の挿入、削除を頻繁に行うようなListが必要な場合にLinkedListを検討すると良さそう
HashSetとLinkedHashSetとTreeSet
- HashSetは順序を保証しない
- 要素の追加、削除、検索は平均して計算量O(1)
- LinkedHashSetは内部的にハッシュテーブルとリンクリストを組み合わせて使用する
- 要素の挿入順序を保証する
- 要素の追加、削除、検索は平均して計算量O(1)
- TreeSetは自然順序もしくはコンストラクタで指定されたコンパレータによる順序に従ってソートされる
- 要素の追加、削除、検索は計算量O(log n)
- ソートされた順序が必要な場合に使える
HashMapとLinkedHashMapとTreeMap
Setとほぼ同じ
クラスとインターフェース
基本
package org.example;
public class ClassDemo {
public static class User {
// プロパティ
private String name;
private int age;
// コンストラクタ
public User(String name, int age) {
this.name = name;
this.age = age;
}
// getter / setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public String hello() {
return "Hello, " + this.name;
}
}
public static void demo() {
var user = new User("Tanaka", 20);
System.out.println(user);
System.out.println(user.hello());
}
}
抽象クラス
- 抽象化したいフィールドを宣言できる
- 抽象化したい関数を定義できる
- abstract修飾子をつけた関数を宣言することで継承先に関数の実装を強制できる
package org.example;
public abstract class AbstractUser {
private String name;
private int age;
protected AbstractUser(String name, int age) {
this.name = name;
this.age = age;
}
public String hello() {
return "Hello, " + this.name;
}
public abstract String greet();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
インターフェース
- インターフェースは振る舞い
- abstract classとの違いはabstract classはクラスなので多重継承できないのとフィールドや関数を定義できる
- インターフェースもデフォルト関数定義できるようになったので関数の実装を定義できるが、これはJavaの機能拡張の過程でしょうがなく入れたみたいなことを聞いたことがあるので積極的に使う機能ではないのかもしれない
- インターフェースはあくまで振る舞いで実装を書くべきでない
- フィールドは持つことができないが定数は持つことができる
- しかし、定数のみを持つ定数インターフェースはアンチパターンなのでやるべきでない
- インターフェースのデフォルト実装について
https://qiita.com/yonetty/items/d6c1375c9a4a523be3c5 - ただ単に共通した実装が欲しくてmixin的な使い方をし、そのインターフェースが各所で使われた日にはデフォルトの実装の変更が全ての実装箇所で問題を起こす可能性がある
- 継承より委譲と言われる良い例
- 慣れないうちは使うべきでない
public interface Greetor {
void greet();
// 定数 public static finalは省略できる
String HELLO = "Hello";
// デフォルト実装
default void hello() {
System.out.println(HELLO + "!!");
greet();
}
}
定数とenum
- 定数は
final
をつけることで初期化してから値が変更されないことを保証する - 定数は一般的に
static
をつけることでクラス間でインスタンスを共有する - なので定数は一般的に
public static final
をつけることになる - 定数は全て大文字のスネークケースで書くことが多い
- 定数はアプリ内でよく使う、不変な値を宣言するときに使うと良い
- なんでもかんでも定数にするのはよくない
- 定数を管理する方法として以下がある
- 定数クラスを作って定数だけ置く(継承できないようにfinal & インスタンス作れないようにprivateコンストラクタで)
- 定数インターフェース(インターフェースの使い方ではない)
- enum
- 基本的にはenumが推奨
- enumはシングルトンであることを保証できるし、関数やフィールドも定義できて拡張性がある
package org.example;
import java.util.EnumSet;
import java.util.Optional;
import java.util.regex.Pattern;
public class ConstantsDemo {
// 定数
public static final String MESSAGE_1 = "Hello, World!!";
// Patternオブジェクトなんかは毎回インスタンス化せずに定数で宣言しておく方が
// 毎回インスタンスを生成せずに済むため効率が良いことが多い
public static final Pattern MOBILE_PHONE_PATTERN = Pattern.compile("^\\d{3}-\\d{4}-\\d{4}$");
// enum
enum StatusCode {
OK(200, "Success"),
NOT_FOUND(404, "Not Found")
;
// フィールド
private final int code;
private final String message;
// コンストラクタ
StatusCode(int code, String message) {
this.code = code;
this.message = message;
}
// code -> enum
public static Optional<StatusCode> codeOf(int code) {
return EnumSet.allOf(StatusCode.class).stream().filter(sc -> sc.code == code).findFirst();
}
}
public static void demo() {
System.out.println(MESSAGE_1); // Hello, World!!
// MESSAGE_1 = ""; 定数なので値の上書きはできない、不変
System.out.println(StatusCode.OK);
System.out.println(StatusCode.NOT_FOUND);
StatusCode.codeOf(200).ifPresent(System.out::println);
StatusCode.codeOf(404).ifPresent(System.out::println);
StatusCode.codeOf(500).ifPresentOrElse(System.out::println, () -> {
throw new IllegalArgumentException("サポートしていないステータスコードです");
});
}
}