EurekaMoments

ロボットや自動車の自律移動に関する知識や技術、プログラミング、ソフトウェア開発について勉強したことをメモするブログ

Javaに入門して印象的だったことのまとめ

背景

上記書籍を読んで、Javaの基本および、C++やPythonのような
多言語との違いがいろいろわかったので、その中から特に
印象的だったものをメモしておく。

目次

メモ

ファイル名は「クラス名.java」とする

C++やPythonでも同じように大抵はするけど、しなきゃいけない決まりもない。
Javaのようにマストとするのはコードを分かりやすくできてよいと思った。

1バイトのデータはbyte型

C/C++ではcharなのに対してJavaではbyteとなる。
逆に、Javaにおけるcharは1つの文字となる。

定数の宣言ではfinalとつける

定数を宣言する際は、変数宣言にfinalと記述する。
個人的にはC++みたいにconstとつける方がわかりやすくて好み。

文字列を記述する際のエスケープシーケンス

通常では2つの"で囲まれた文字列が文字列リテラルと見なされる。
"を文字として扱うには、エスケープシーケンス¥を用いて¥"とする。

整数型としてのchar型

char型は内部的には0~65535の範囲の数値も扱える。
でも基本的には文字を扱うために使われる。
自分としてもその方が、他の型と使い方が区別出来て
わかりやすくなるからよい。

文字列を数値に変換する命令

int n = Integer.parseInt(数値として解釈したい文字列);

コードとしては少し長いなと思った。
Pythonみたいにint()やstr()くらいの短さで出来たらいいのに。

文字列の比較

C++などでは文字列の比較は下記のようなコードで行うが、

if (str == "hoge") {...}

Javaでは下記のようにする。

if (str.equals("hoge")) {...}

シンプルさでは圧倒的に==だけど、
Javaみたいにequals()を使うなら、==を=と間違える
みたいなコーディングミスを防げて良さそう。

配列変数の作成

Javaでは下記のように代入する要素の型の後ろに[]をつけて宣言する。

int[] arrays;

そして、下記のようにnew 演算子を使って決まった要素数の配列を作成する。

arrays = new int[5];

あるいは、下記のようにすれば宣言と作成を同時に行える。

int[] arrays = new int[5];

ここは、C++やPythonと比べて一番最初にギャップを感じたところ。
覚えるまでしょっちゅう間違えそう。

ガベージコレクション

C++ではnew で割り当てたメモリはdeleteで解放するようになっているが、
Javaではガベージコレクション(GC: garbage collection)が常に動き、
メモリ上のゴミを自動的に探して片づけてくれる。

さてこれはどっちが良いのだろうか。
わざわざメモリの解放を明示しなくてよいのは楽だが、
deleteなどでメモリ解放されてることをはっきり示して
くれるコードの方が、可読性は高い気もする。

引数に配列を用いる

配列を宣言するときと同じ書き方で、下記のように
引数で配列を使える。

public static void function(int[] array) {...}

これは、アドレス情報のみである参照渡しとなる。
戻り値に使うこともできる。

複数のクラスファイルをまとめたJARファイル

Javaプログラムの完成品は、複数のクラスファイルの集合体のため、
配布するには全てのクラスファイルを渡さなければならない。
それらを1つにまとめるファイル形式としてJAR(Java ARchive)がある。

import宣言は入力軽減機能

Javaではいっさいの宣言をすることなく、JVMが扱えるすべてのクラスを
常時使うことができる。なのでimport文は書かなくてもいい。
この点がC/C++やPythonのinclude文やimport文とは違うところ。
ただ、それだとクラスを呼び出す際のコードの行が長くなるので、
やはりimport文は積極的に使いたいところ。

パッケージ名の付け方について推奨されるルール

パッケージ名が異なればクラス名は重複してもいい。
つまり、パッケージ名は衝突してはいけない。
Javaでは、自分(自社)が保有するインターネットドメインを
前後逆順にしたものから始まるパッケージ名を付けることを推奨している。
hoge.huga.comというドメインを保有する企業であれば、
com.huga.hogeで始まるパッケージ名を使うということになる。
C++やPythonでも命名規則は存在するけど、こういう外部の人間が
開発したものとの名前の衝突を意識するというのはJava特有だと思った。

Java APIの使い方

Javaが提供する膨大な数のAPIの使い方を知るには、
下記のような公式リファレンスを読む必要がある。
docs.oracle.com

docs.oracle.com

暗黙のコンストラクタ

Javaにおける全てのクラスは、必ず何らかのコンストラクタを実行する
ことになっている。一つもコンストラクタが定義されていないときは、
引数なし+処理内容なしのコンストラクタが自動的に追加される。
ちゃんと明示的にコンストラクタを書くようにした方が可読性が良いだろう。
でも、オーバーロードで引数の異なる同名のコンストラクタを定義する
のは好みじゃない。
それくらいなら別々の用途で使うものとしてクラスを分けることを考えたいな。

extendsによる継承

Javaでは、下記コードのようにextendsを使ってクラスの継承を実現する。
この場合、Hogeが元となるクラスであり、それを継承してFugaを宣言する。 また、複数のクラスを親としてもつ多重継承は許されていない。

public class Fuga extends Hoge {
    // 親クラスとの「差分」メンバ
}

C++やPythonよりも継承していることが明示的に感じられて好きかもしれない。

継承を禁止するためのfinal

下記コードのように、宣言時にfinalが付けられているクラスは継承できない
ようになっている。

public final class Hoge {
    public static void function() {
        // メソッド
    }
}

定数を宣言するときと同様で、むやみやたらに継承されて元のメソッドを
オーバーロードされたりするのを防ぐ目的のよう。
また、メソッドでもfinalを付ければ、それは子クラスではオーバーロード
できないようになる。

public final void function() {
    // メソッド
}

親クラスのコンストラクタの呼び出し

全てのコンストラクタは、その先頭で内部インスタンス部分(親クラス)の
コンストラクタを呼び出さなければならないルールがある。
なので子クラスのコンストラクタは、下記のようにしなければならない。

public Hoge() {
    super(); // 親クラスのコンストラクタ
    // 子クラスのコンストラクタ部分
}

仮にここでsuper()を呼び出さないような実装をしていた場合は、
コンパイラが自動的にsuper();を挿入する。

抽象メソッドとして宣言するabstract

abstractを付けることで抽象メソッドとして宣言できる。
これがないと、そのメソッドは継承先でオーバーロードされるべき
なのか、本当に何もしない空のメソッドなのか区別できなくなる。

public abstract class Hoge {
    public abstract void function(int x); // {}は付けない
}

また、抽象メソッドを持つクラスは、同様にabstractを付けて抽象クラス
として宣言する必要がある。

抽象クラスはnewによるインスタンス化が禁止される

抽象クラスをnewでインスタンス化しようとすると
コンパイルエラーになる。
抽象クラスはメソッドの中身が確定していないので、
そのままインスタンス化しても悪影響を及ぼす危険性が
ある。それを未然に防げるこういった仕組みはうれしい。

特に抽象度が高い抽象クラスを宣言するインターフェース

全てのメソッドが抽象メソッドで、フィールドを1つも持たない
クラスはインターフェースとして扱える。

public interface Hoge {
    public abstract void function();
}

抽象メソッドは通常abstractを付けて宣言するが、
インターフェースが持つメソッドは自動的に
publicかつabstractになるので、それらを省略して
次のようにも書ける。

public interface Hoge {
    void function();
}

インターフェースを継承する場合は、extendsじゃなくて
implementsを使う。まさに実装という意味の言葉を使う
ようにしてくれてるのは分かりやすくていい。

キャストしても大丈夫かチェックするinstanceof演算子

安全にキャストできるかを判定することができる。
安全とは、「指定の型に代入しても絵として嘘にならないか」を
意味する。

if (c instanceof Hoge) {
    Hoge h = (Hoge)c;
}

キャストしてるせいで思うような処理にならないなんて
こともよくあるので、こうやって事前に判定できる仕組みは
嬉しい。

最低限の機能を備えたObjectクラス

Javaでは、全てのクラスの先祖であるObjectクラスがある。
こうすることで、全てのクラスをObjectクラスとざっくり
みなせるようになる。
また、Javaのクラスであれば最低限必要とされている下記の
クラスを持たせることができる。

  1. equals(): あるインスタンスと自分自身とが同じかを調べる
  2. toString(): 自分自身の内容の文字列表現を返す

これらにより、クラスの種類を気にせず、同じ方法で内容を
確認できるというメリットがある。

これは、C++やPythonみたいな他の言語でクラスを定義する
ときに真似したくなるいい仕組みだと思う。子クラス側で
忘れずにオーバーライドしないといけないのが面倒だけど。

staticメソッドはインスタンスを1つも生み出さずに呼び出せる

mainメソッドが最初に呼び出される際は、まだJavaの仮想世界には
1つもインスタンスが生成されていない。
そのため、mainメソッドはstaticでないと動かせないことになる。

StringBuilderを用いた文字列の連結

Javaにおける文字列の連結は+演算子を用いるのが最も簡単だが、

String s = "hoge" + "fuga";

他にもStringBuilderを用いる方法があり、その方が高速とされている。
StringBuilderインスタンスは、連結した文字列を蓄えるバッファを内部に持つ。
例えば、文字列を1万回連結するときは下記のようなコードになる。

public class Hoge {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder(); // インスタンス生成
        for (int i = 0; i < 10000; i++) {
            sb.append("fuga"); // appendメソッドで文字列をバッファに追加
        }
        String s = sb.toString(); // 最後にtoString()を呼び、完成した連結済み文字列を取り出す
    }
}

文字列連結で+演算子が遅い理由

Stringクラスは、インスタンス化の後に内容が絶対に変化
しないように設計された不変(immutable)なクラスとなっている。
そのため、+演算子を用いた方法では、連結のたびに新しい
Stringインスタンスがnewで生成され、古いインスタンスは
捨てられることになる。
newによるインスタンス生成は、JVMに対して大きな負荷が
かかる処理のため、全体でとても遅い処理になってしまう。

C++やPythonだといつも+演算子を使っているので、
その癖でつい同じようにしてしまいそう。
こういう速い仕組みが用意されてるのは嬉しいし、
+演算子が遅い理由もしっかり理解しておくのは大事。