JavaSilverSE11の黒本を読み込んでこれは大事だなって思ったこと4選をまとめます。
- オーバーライドとオーバーロード
- 配列の初期化方法いろいろ
- clone()メソッド
- 関数型インターフェース
オーバーライドとオーバーロード
まずここで一番引っかかったポイントは継承したメソッドをどのように使うかでした。
基本的にサブクラスはスーパークラスのメソッドを扱えますが、宣言する型によって使えたり使えなかったりします。
パターン①:最も基本的なパターン
public class A{
public void sample(){};
}
public class B extends A{
public void test(){};
}
public class Main{
public static void main(String[] args){
A a1 = new A(); //1 A型にAのインスタンスを代入
A a2 = new B(); //2 A型にBのインスタンスを代入
B b1 = new A(); //3 B型にAのインスタンスを代入
B b2 = new B(); //4 B型にBのインスタンスを代入
}
}
例えばこのようなサンプルコードがあった場合、
1は割愛。2はA型のローカル変数a2にBのインスタンスを渡しているのでtest()メソッドも使えそうな気もしますが、a2.test()
はコンパイルエラーになります。A型で宣言しているのでa2のスコープはAクラス内のメソッドに限定されます。
3はコンパイルエラーになります。
4も自明なので割愛。
じゃあこういう場合はどうなんだっていうパターンがあります。
パターン②:サブクラスでオーバーライドしているパターン
public class A{
public void sample(){
System.out.println("A");
};
}
public class B extends A{
@Override
public void sample(){
System.out.println("B");
};
public void test(){};
}
public class Main{
public static void main(String[] args){
A a2 = new B();
a2.sample();
}
}
この時、コンソールには以下のように表示されます。
B
基本的なパターンで、A型の変数はAクラス内のメソッドしか扱えないはずなのにと思いましたが、以下のような流れで決まるとのこと(黒本曰く)
- 呼び出されたメソッド
sample()
がオーバーライドされているかどうかをインスタンス内で探す - あればオーバーライドしているメソッド、すなわちBクラスで定義されている
sample()
を実行 - なければA型の
sample()
を実行
つまり、オーバーライドのメソッドを優先的に実行する流れであるようです。
次にこういう場合はどうでしょう?
パターン③:スーパークラスとサブクラスに同名のメソッドがあるパターン
public class A{
public void sample(){
System.out.println("A");
};
}
public class B extends A{
public void sample(String str){
System.out.println(str);
};
public void test(){};
}
public class Main{
public static void main(String[] args){
A a2 = new B();
a2.sample();
}
}
この場合は次のようになります。
A
Bクラスで定義しているsample()
はオーバーライドの条件を満たしていないため、同名の別のメソッドとして認識されます。従って、mainメソッドでsample()
に引数を渡していなくてもコンパイルエラーになることはありません。ちなみにこの時、サブクラスBのsample()
に「@Override」アノテーションを付けたらコンパイルエラーになります。
オーバーライドの条件
- 戻り値型が同じかそのサブクラス(共変戻り値)
- シグニチャが同じ
- アクセス修飾子が同じかより緩いもの
- 例外がある場合、同じ型かそのサブクラス
次にIFを実装したらどうなるでしょう?
パターン④:インターフェースを実装したパターン
interface C{
public void method();
}
public class A{
public void sample(){
System.out.println("A");
};
public void method(){
System.out.println("C");
};
}
public class B extends A implements C{
@Override
public void sample(){
System.out.println("B");
};
public void test(){};
}
public class Main{
public static void main(String[] args){
A a2 = new B();
a2.method();
}
}
コンソールはこんな感じ
C
インターフェース、または抽象クラスに定義された抽象メソッドは直近の具象クラスで必ず実装しなければいけないという決まりごとがあります。
→これやらないとコンパイルエラーになります。
クラス図で表すとこうなります。
一見、Cインタフェースのmethod()
をBクラスで実装していないように見えますが、コンパイルエラーになりません。理由は、BクラスがAクラスを継承しているため、Aクラスのmethod()
がCインタフェースの実装とみなされるためです。
最後に、複雑なパターンを考えます。
パターン⑤:オーバーライドとオーバーロードが混在しているパターン
public class A{
public void sample(object obj){
System.out.println("A");
};
}
public class B extends A{
@Override
public void sample(Object obj){
System.out.println("B");
};
public void sample(String str){
System.out.println("C");
};
public void test(){};
}
public class Main{
public static void main(String[] args){
B b = new B();
b.sample("sample");
}
}
次のように表示されます。
C
BクラスではAクラスのsample()
をオーバーライドしつつ、オーバーロードもしています。mainメソッドではB型の変数bにBインスタンスを渡しています。この場合、b.sample("sample")
で渡している引数がString型なのでObject型でも受け取れますが、JVMはより厳密に定義している方を優先するため、オーバーロードされているメソッドが実行されます。
継承、オーバーロード、オーバーライドが絡むと複雑な挙動を示すように思いますが、JVMが処理する優先事項とアクセス修飾子、クラスの関係性に注意すれば大丈夫かと思います。
配列・リストの初期化式いろいろ
次に、配列の初期化方法についてまとめます。これも最初は混乱しましたが整理しました。
Integer[] list1 = {1,2,3}; //パターン1
List<Integer> list2 = List.of(1,2,3); //パターン2
List<Integer> list3 = Arrays.asList(1,2,3); //パターン3
List<Integer> list4 = new ArrayList<Integer>(list1); //パターン4
List<Integer> list5 = new ArrayList<Integer>(list2); //パターン5
List<Integer> list6 = new ArrayList<Integer>(Arrays.asList(new Integer[]{1,2,3})); //パターン6
黒本で見かけた配列、およびリストの初期化式はこんな感じです。
パターン1~3は配列なので要素数の追加・削除はできません。特にList型で宣言しているパターン2,3の配列にadd()
を使って要素の追加を書くとコンパイルエラーにはならず、UnsapportOperationExceptionが発生します。
new ArrayListのインスタンスを渡しているパターン4~6はリストなので要素の追加・削除ができます。
clone()メソッド
配列を複製する時に使うメソッドですが、どれが共通要素で何がオリジナルなのかが全く分からなかったので理解した内容をまとめます。
int[] array1 = {1,2,3};
int[] array2 = array1.clone();
System.out.println(array1[0] == array2[0]);
System.out.println(array1 == array2);
System.out.println(array1.equales(array2));
実行すると結果はこうなります。
true
false
false
array1.clone()
は具体的に以下の流れ
- メモリ空間に{1,2,3}が複製される
- 複製されたアドレスを返す
- array2にアドレスが渡される
従って、array1とarray2が参照しているアドレスは異なりますが、配列要素の内容は同じです。
"=="は同一性を評価するため、array1 == array2
はfalseになります。Object型のequals()
も同様に同一性を評価するため、array1.equales(array2)
もfalseになります。
関数型インターフェース、ラムダ式、メソッド参照
ラムダ式については黒本著者が語るやさしくないJavaの動画を見てとても理解が深まった記憶があります。是非、見ていただきたい。
初見では理解できない書き方ですが、段階的にコード量が短くなっていく過程で深く理解できます。
代表的な関数型インタフェースにConsumer<>インタフェースがあります。
Consumer<>はabstract accept()
という抽象メソッドが一つ定義されています。
これを使って、リストの中身を表示するためには以下のように書いていました。
第一段階
import java.util.List;
import java.util.ArrayList;
import java.util.function.Consumer;
class Sample implements Consumer<Integer> {
@Override
public void accept(Integer num){
System.out.println(num);
}
}
class Main{
public static void main(String[] args){
List<Integer> list = new ArrayList<>(List.of(1,2,3,4)); // リストを生成
Sample sample = new Sample();
for(Integer i : list){
sample.accept(i);
};
}
}
ここでSampleクラスにComsumer<>を実装する無駄を省くためにMainクラスへ直接書き込むようにします。その際、Consumer型でaccept()
を定義します。
第二段階
import java.util.List;
import java.util.ArrayList;
import java.util.function.Consumer;
class Main{
public static void main(String[] args){
List<Integer> list = new ArrayList<>(List.of(1,2,3,4)); // リストを生成
Consumer<Integer> sample = new Consumer<Integer>() {
@Override
public void accept(Integer num){
System.out.println(num);
}
};
for(Integer i : list){
sample.accept(i);
};
}
}
これで割とすっきりしましたがまだです。ここでラムダ式を導入します。
第三段階
import java.util.List;
import java.util.ArrayList;
import java.util.function.Consumer;
class Main{
public static void main(String[] args){
List<Integer> list = new ArrayList<>(List.of(1,2,3,4)); // リストを生成
Consumer<Integer> sample = num -> System.out.println(num);
for(Integer i : list){
sample.accept(i);
};
}
}
かなりすっきりしてきました。ちなみにラムダ式の書き方もかなり省略しています。
引数の型を省略したり、{}
も省略できますが本来はこちらの書き方です。
Consumer<Integer> sample = (Integer num) -> {
System.out.println(num);
};
ここからまだいけます。次は拡張for文を簡略化していきます。
第4段階
import java.util.List;
import java.util.ArrayList;
import java.util.function.Consumer;
class Main{
public static void main(String[] args){
List<Integer> list = new ArrayList<>(List.of(1,2,3,4)); // リストを生成
Consumer<Integer> sample = num -> System.out.println(num);
list.forEach(sample);
}
}
だいぶ良くなってきました。Listインタフェースにある、forEach(Consumer<> c)
を使います。このメソッドはConsumer型の変数を引数にするため使えます。ちょうど、Listインタフェースのcompare()
がComparator型の変数を引数にするみたいな感じです。
ここで注目すべきはforEach()
にsampleを渡しているところです。ここも省略できそうなのでします。
第5段階
import java.util.List;
import java.util.ArrayList;
class Main{
public static void main(String[] args){
List<Integer> list = new ArrayList<>(List.of(1,2,3,4)); // リストを生成
list.forEach(num -> System.out.println(num));
}
}
これで行数は最大に省略できました。もはや関数型インタフェースのConsumerも必要なくなりました。次で最後です。
最終段階
import java.util.List;
import java.util.ArrayList;
class Main{
public static void main(String[] args){
List<Integer> list = new ArrayList<>(List.of(1,2,3,4)); // リストを生成
list.forEach(System.out::println);
}
}
メソッド参照を使いました。もはや引数も必要なくなりました。
第1段階の頃とくらべると原型が無いほどすっきりしましたが、表示される結果は同じなので不思議な気もします。
黒本ではこの流れで説明されており、改めて自分でコードを書くとより深く理解することができます。内容は黒本と一緒ですが、自分なりにまとめてみました。
おわり
今回取り上げた4つ以外にも大切なポイントはたくさんありますが、だいたいは暗記なので覚えていれば大丈夫って感じですね。ロジックを理解しておくといいものだけまとめました。