顧客フロントSEのIT勉強ブログ

2022 Japan AWS Top Engineer / 2022-23 Japan AWS Certifications Engineer。AWS認定12冠、情報処理試験全冠。顧客フロントSEがなるべく手を動かしながらIT技術を学んでいくブログです。

【資格】Java Silverに合格した勉強法

これまではAWSを中心に勉強してきましたが、アプリ開発のスキルも身に付けるべくJavaの勉強を開始し、先日Java Silver(正式名称 Oracle Certifiied Java Programmer, Silver SE 11)に合格しました!
今日は「Java Silver」の勉強法と学んだことを記していきます。

(2022年9月7日追記)
Java Goldも合格しました!その勉強法、出題範囲のまとめはこちら。

準備期間と結果

準備期間は2022年4月4日~5月6日と約1ヵ月でした。

元々はJavaを含めプログラミング言語の知識はほとんどなく、「コンストラクタ?インターフェース??アノテーション???」という状態でした。そこから約1か月間でオブジェクト指向とはどのようなもので、それを実装する方法を理解できたのは自分にとってプラスになったと思います。

結果は正解率:92%で、余裕を持って合格することが出来ました!

合格までの勉強法

1.参考書(紫本)

参考書として「ラクル認定資格教科書 Javaプログラマ Silver SE11」、通称紫本を使いました。 まずはこの本で一連の内容をざっと把握し、各章末尾にある練習問題で感触をつかんでいきました。
とは言え、最初の正解率は2~3割程度だったと思います。

2.問題集(黒本)

紫本だけだと問題の量が足りないので、メインの問題集として「徹底攻略 Java SE 11 Silver問題集」、通称黒本を使いました。いろんな方のレビューにもあるように極論この本だけやりこんでおけば大丈夫です。
実際、本番の試験でも「黒本で似たような問題を見たな・・」というものがいくつかありました。
2~3周繰り返し解き、最終的には9割以上解ける状態で本番を迎えました。

3.試験内容チェックリスト

本番数日前になってようやく、Oracle公式の試験内容チェックリスト(リンクはこちら)を確認して、各用語を見れば「大体こういう内容だな」と理解できる状態になっていることを確認しました。

今回学んだこと

少しググれば試験内容をまとめたサイトが沢山ヒットするので、ここでは試験範囲を網羅的に取り上げるのではなく、メインどころに絞って整理しようと思います。
今回取り上げないもので重要なポイントは「基本データ型/参照型の違い」、「if else文、switch case文の構文」、「try-catch-finally文による例外処理」、「パッケージ/モジュールの概念」くらいかと思います。

1.コンストラク

コンストラクタとは、クラスをインスタンス化するときに自動実行されるメソッドのことです。メンバ変数(Static変数、インスタンス変数)の初期化などが主な目的となります。

以下のようにクラス名と同名で、戻り値の型指定がないメソッドを定義するとコンストラクタになります。

public class Test {
   // 下記2つともコンストラクタ
   public Test(){}
   public Test(int num){ 
      System.out.println(num);
   }
}

インスタンス化するときのnew演算子の後ろに続いているのはコンストラクタ名です。Test test = new Test(10); のように引数を含む形でインスタンス化すると、それに対応するコンストラクタを呼び出せます(後述のオーバーロードの一種)。

◆試験におけるポイント◆
・コンストラクタを定義しないとデフォルトコンストラクタ(引数なし、処理なし)が自動追加。
・コンストラクタにアクセス修飾子(public, etc)は何でも付与可能。
インスタンス化されないstaticクラスはstatic初期化子で初期化処理を実施。
・コンストラクタから同じクラスの別コンストラクタを呼び出すときはthisを使う。
・コンストラクタからスーパークラス(後述)のコンストラクタを呼び出すときはsuperを使う。

2.継承

あるクラスのフィールドやメソッド定義を引き継いで新しいクラスを定義すること継承と言います。コードを再利用できるため実装負荷の軽減や二重メンテを避けられるというメリットがありますが、「どのクラスが継承してこのコードを使っているのか」が分からず保守性が下がるため、実際はポリモーフィズム実現が継承の主目的だそうです。

上のクラス図だとEmployeeクラスを元に、SalesとEngineerクラスを定義しています。Employeeのような継承元クラスを「スーパークラス」、SalesやEngineerのような継承先クラスを「サブクラス」と呼びます。これらを実装すると下記のようになります。

public class Employee {
   public String name;
   public String hireDate;
   public void inputTimeCard(String startTime, String endTime){
      // 何らかのコード
   }
}
public class Sales extends Employee {
   public void doSales(){ 
      // 何らかのコード
   }
}
public class Engineer extends Employee {
   public void developSystem(){
      // 何らかのコード
   }
}

サブクラス指定時にextendsスーパークラスを指定します。このSalesクラスをインスタンス化するとEmployeeクラスで定義したname変数やinputTimeCardメソッドを使用できます。
また、下記のようにサブクラスのインスタンススーパークラスの変数に代入することが出来ます。こうすることで複数種類のサブクラスを一括して扱えるようになるというメリットがあります。

class test{
  public static void main(String[] args){
   superClass obj[] = new superClass[3]; // 各サブクラスのインスタンスをスーパークラスの配列に代入 obj[0] = new subClassA(); obj[1] = new subClassB(); obj[2] = new subClassC(); for (int i = 0 ; i < 3 ; i++){ obj[i].dispName(); //for文のイタレーションで処理可能 } } } class superClass{ public void dispName(){ System.out.println("未定義です"); } } class subClassA extends superClass{ public void dispName(){ System.out.println("名前はXXXです"); } } class subClassB extends superClass{ public void dispName(){ System.out.println("名前はYYYです"); } } class subClassC extends superClass{ public void dispName(){ System.out.println("名前はZZZです"); } }
◆試験におけるポイント◆
・多重継承(複数のスーパークラスを持つこと)はできない。
・継承してもコンストラクタとprivateなフィールド・メソッド定義は引き継げない。
・スーパークラス、サブクラスの両方で同名フィールドを定義した場合、それぞれが別物としてメモリに展開される。
・サブクラスをインスタンス化すると、スーパークラスのコンストラクタ→サブクラスのコンストラクタの順に実行される。

3.オーバーロード、オーバーライド

この2つを混同しがちですが、
・オーバーロード:引数の数 or 型 or 順番が異なる同名のメソッド・コンストラクタを定義すること
・オーバーライド:サブクラスで、スーパークラスから継承したメソッド定義を上書きすること
です。個人的なイメージとしては、オーバーロードはメソッド定義を”横に広げる”のに対し、オーバーライドはメソッド定義を”上に被せる”感じです。

■オーバーロード

オーバーロードする際は、次のように引数定義(数 or 型 or 順番)が異なる同名のメソッドを定義します。

class Animal{
   private String name;
   private int age;

   // 以下3つのメソッドをオーバーロード
   public void setAnimal(String name) {
      this.name = name;
      System.out.println("名前は" + name + "です");
   }
   public void setAnimal(int age) {
      this.age = age;
      System.out.println("年齢は" + name + "歳です");
   }
   public void setAnimal(String name, int age) {
      this.name = name;
      this.age = age;
      System.out.println("名前は" + name + "で、年齢は"+ age + "歳です");
   }
}

■オーバーライド

オーバーライドする際は、メソッド名・引数ともに全く同じものをスーパークラス、サブクラスで定義します。サブクラスをインスタンス化して該当メソッドを呼び出すとオーバーライド後の実装で動きます。

class Test{
  public static void main(String[] args){
    B1 b1 = new B1();
    b1.disp();  // 「サブクラスです」と表示される

    B2 b2 = new B2();
    b2.disp(); // 「スーパークラスです」と表示される
  }
}

class A{
  public void disp(){
    System.out.println("スーパークラスです");
  }
}

class B1 extends A{
@Override
public void disp(){ System.out.println("サブクラスです"); } } class B2 extends A{ }

途中に記載されている@Overrideオーバーライドアノテーションと呼ばれるもので、後続がオーバーライドしたメソッドであることを明示します。コードの可読性を向上させるとともに、引数間違いや継承元メソッドがprivate修飾されているなどでオーバーライドしたつもりが正しくオーバーライドできていなかった場合、コンパイルエラーにさせてコーディングミスに気付かせるという目的で使います(アノテーションをつけないと別物のメソッドとして正常定義されてしまい、コーディングミスに気づけない)。

◆試験におけるポイント◆
・戻り値の型が違うだけ、仮引数名が違うだけではオーバーロードにならない。
・mainメソッドも別引数でオーバーロード可能(ただしエントリポイントになるのはString型配列が引数のmainメソッドだけ)
・オーバーライドするメソッドの戻り値型は、その戻り値型のサブクラスになら変更可能(共変戻り値)。

4.抽象クラス、インターフェース

これも混同するというか、使い分けが難しく感じるポイントですが、
・抽象クラス:複数クラスに共通する処理を1つにまとめたもの。IS A関係。
・インターフェース:メソッドの名称・型・引数のテンプレート。CAN DO関係。
という表現が自分の中ではしっくり来ています。

■抽象クラス

抽象クラスの例として「自動車」クラスを考えます。自動車が出来ること(メソッド)として「ハンドル」、「アクセス」、「ブレーキ」は共通していますが、日本車なら右ハンドル、輸入車なら左ハンドルというように「ハンドル」の実装内容は車の種類によって違います。

そこで「ハンドル」という抽象メソッドを持つ「自動車」クラスを抽象クラスとして宣言し、ハンドルの具体処理は継承したサブクラスの中で実装させます。抽象クラスには具体的な処理を定義したメソッド(具象メソッド。上記なら「アクセル」と「ブレーキ」)を含めても良いです。

具体的にコードで表現すると次のようになります。抽象クラス・抽象メソッドにはabstract修飾子を付けます。

public abstract class Car{
  abstract void handle(int angle);
  void accelerator() { /* アクセルの実装 */ }
  void brake() { /* ブレーキの実装 */ }
}

class JapaneseCar extends Car{
  @Override
  void handle(int angle){ /* 右ハンドルの実装 */ }
}

class ImportCar extends Car{
  @Override
  void handle(int angle){ /* 左ハンドルの実装 */ }
}

■インターフェース

インターフェースの例として、バイクや電車など他の乗り物にスコープを広げてみます。どれも実装は全く違いますが、速度調整するための「アクセル」「ブレーキ」、方向調整するための「ハンドル」という機能名は共通しています。

このように機能(=メソッド)の名称や型、引数などをテンプレート化して、「アクセルといえばスピードアップするための機能だよね」という共通言語を作るのがインターフェースです。

「自動車」クラスのように抽象クラスがインターフェースを実現してもよいし、「バイク」クラスのように複数のインターフェースを多重実現しても良いです。コードで表現すると次のようになります。

interface AdjustSpeed {  // publicが自動付与される
  void accelerator()  // public abstractは自動付与されるため省略可。
  void brake()
}
interface AdjustCourse {
  void handle(int angle)
}

public abstract class Car implements AdjustSpeed, AdjustCourse{
  @Override
  void accelerator(){ /* アクセルの実装 */ }
  @Override
  void brake(){ /* ブレーキの実装 */ }
}

class Bike implements AdjustSpeed, AdjustCourse{
  @Override
  void handle(int angle){ /* ハンドルの実装 */}
  @Override
  void accelerator(){ /* アクセルの実装 */ }
  @Override
  void brake(){ /* ブレーキの実装 */ }
}

class Train implements AdjustSpeed{
  @Override
  void accelerator(){ /* アクセルの実装 */ }
  @Override
  void brake(){ /* ブレーキの実装 */ }
}
◆試験におけるポイント◆
・抽象クラスはpublic or protected。インターフェースはpublicのみ。どちらも直接インスタンス化はできず、継承・実現前提なのでfinal修飾は不可。
・抽象クラスはインスタンス変数、クラス変数などなんでもフィールド定義可能。
・インターフェースにはpublic static finalな定数だけフィールド定義可能。
・インターフェースもdefaultメソッドという具象メソッドが定義可能。

5.関数型インターフェース、ラムダ式

ラムダ式というコーディング規約に触れるためには、先に「関数型インターフェース」を理解する必要があります。これは前述のインターフェースの一種で「抽象メソッドが1つだけのインターフェース」です。これにより、インターフェース名を指定すれば呼び出したいメソッドも一意に決まる、という特徴を持ちます。

もちろん自分で定義しても良いのですが、java.util.functionパッケージ内に基本的な関数型インターフェースが定義されています。

Consumer

消費者。引数を1つ渡されるが何も返さない。
抽象メソッドは「void accept(T t)」。

Supplier 供給者。引数は無く、結果を1つ返す。
抽象メソッドは「T get()」。
Predicate

断定。引数を1つ渡すと、boolean型の結果が返ってくる。
抽象メソッドは「boolean test(T t)」。

Function 関数。引数を1つ渡すと、別の型の結果が1つ返ってくる。
抽象メソッドは「R apply(T t)」。
UnaryOperator

単項演算子。引数を1つ渡すと、同じ型の結果が1つ返ってくる。
Functionをスーパーインターフェースに持つ。抽象メソッドは「T apply(T t)」。

これらはインターフェースなので他クラスで実現させることが前提なのですが、関数型インターフェースに限ってはその特徴を生かしてより簡易なコーディングで実現しちゃおう、というのがラムダ式というコーディング規約です。

ラムダ式の基本形は「①抽象メソッドの引数宣言部 -> ②抽象メソッドの実装部」です。①②にはそれぞれ次のパターンがあります。

①抽象メソッドの引数宣言部

パターン 書き方
抽象メソッドに引数が無い場合 ()
抽象メソッドの引数が1つの場合 (引数の型 引数の変数名)
(引数の変数名)
引数の変数名
抽象メソッドの引数が複数の場合 (引数の型1 引数の変数名1, 引数の型2 引数の変数名2, … )
(引数の変数名1, 引数の変数名2, … )

②抽象メソッドの実装部

パターン 書き方
抽象メソッドの戻り値型がvoidの場合 メソッドの内容が1行でかける場合 処理
{処理;}
メソッドの内容が2行以上の場合 {処理1行目;
 処理2行目; …}
抽象メソッドの戻り値型がvoid以外の場合 メソッドの内容が1行でかける場合 処理
{return 処理;}
メソッドの内容が2行以上の場合 {処理1行目;
 処理2行目; …
 return 処理結果;}

コードで表現すると次のようなイメージです。Consumerから始まる一行だけで抽象メソッド(Consumerインターフェースだとacceptメソッド)の実装内容の定義と、インスタンス化までを一度に実施してしまいます。
func変数にそのインスタンスが代入されているので、後は従来通りfunc.acceptの形式でメソッドを呼び出せばよいだけです。

public static void main(String[] args){
   Comsumer<String> func = text -> System.out.println(text);
   func.accept("ラムダ式すごい");  //「ラムダ式すごい」と表示される 
}
◆試験におけるポイント◆
ラムダ式のメソッド実装部で使える変数は、メソッド内で宣言したローカル変数、抽象メソッドの引数、実質的にfinalなローカル変数のみ。
ラムダ式で宣言する引数のスコープは、ラムダ式を囲むブロック。そのブロック内で宣言済の変数名を引数で宣言するとコンパイルエラー。

6.API

APIというとまずWeb APIが思い浮かびます。「Application Programming Interface」の略であることは同じなのですが、JavaにおけるAPIは「汎用的なクラスをまとめたクラスライブラリ」のことを指します。各API(クラス)の仕様は下記サイトが公式です。

既存で色んな便利なクラスが用意されているから上手く使おうね、というのがこの章の内容です。主なクラスとメソッドを紹介します。

■基本クラス

一例として、文字列型を表す「String」もJava APIが提供するクラスの1つです。
Stringで良く利用するメソッドとして以下のようなものがあります。

int length():文字列の長さを返却する
String substring(int beginIndex):beginIndexから最後までの部分文字列を返却
String substring(int beginIndex, int endIndex):beginIndexからendIndex -1までの部分文字列を返却
boolean equals(Object anObject):同じ文字列ならtrue、そうでないならfalseを返す

上記の内、substringメソッドはオーバーロードすることで複数の機能を持たせていますね。

■コレクションクラス

コレクションとは複数要素の集まりのことです。Java SilverではCollectionインターフェースのサブインターフェースにあたるList、Setインターフェースと、Collectionとは別定義ですが同じく複数要素を保持するMapインターフェースを理解しておく必要があります。

Listインターフェースは重複要素を許可し、要素の順番を持つインターフェースです。同様の特徴を持つものとして配列がありますが、配列は変数宣言時に要素数をあらかじめ決めておく必要がある一方、Listは随時要素数を増減させることが出来ます。要素数をあらかじめ決められるのであれば配列のほうが高速のようですが、可変長の場合はListを使うとよさそうです。実装クラスとしてArrayList」クラスがあります。
また、Listインターフェースにはofメソッドというstaticメソッドがあります。staticのためList.of("a","b","c")のように直接呼び出せるのですが、これで変更不可能(つまり固定長かつ更新不可)のListインスタンスが生成されます(プログラマが明示的にnewせずにインスタンスが作られるメソッドをファクトリ・メソッドと呼ぶようです。メソッド定義の中でnewしたインスタンスをreturnする構成なのですが、これはJava Silverの範囲外のお話・・・)。

Setインターフェースは重複要素を持たない、挿入される挿入順を持つインターフェースです。実装クラスとして「HashSet」クラス「TreeSet」クラスがあります。

Mapインターフェースはキーと値が対となった要素を持つインターフェースで、キーの重複はNGです。実装クラスとして「HashMap」クラス「TreeMap」クラスがあります。

ちなみに、これらのインターフェースにはあらゆるオブジェクトを要素として格納できます。例えば「0番目の要素は10、1番目は"ABC"、2番目はJapaneseCarインスタンス・・・」のような無茶苦茶なリストも作れます。これはこれで便利かもしれませんが、読み取る要素の型によって処理内容を変えないといけないという手間も出てきます。
そこで格納する要素の型を限定するために、次のような構文で変数宣言します。

ArrayList<String> list = new ArrayList<String>();

この< >ジェネリクスと呼び、クラスやメソッド定義時に型を決めるのではなく、宣言時に型を定義する機能です。上記の例だとArrayListインスタンスに格納できる要素をStringだけに限定しています。

■Comparatorクラス

java.utilパッケージに含まれる、リストなどのコレクション内の要素をソートする際に使う関数型インターフェースです。抽象メソッドとしてcompareメソッドが定義されています。
compareメソッドの処理内容は個々で実装が求められますが、ルールとして次の仕様にする必要があります。

・1つ目の引数と2つ目の引数を比較し、1つ目が小さいなら負の整数を返す
・1つ目の引数と2つ目の引数を比較し、両者が等しいなら0を返す
・1つ目の引数と2つ目の引数を比較し、1つ目が大きいなら正の整数を返す

コレクションのsortメソッドはcomparatorが上記仕様に沿う前提で、1つ目の引数にソート対象のコレクションを、2つ目の引数にcompareメソッドを実装したcomparatorインスタンスを指定することで要素の並べ替えをしてくれます。実装例は下記です。

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class SampleClass {
  public static void main(String[] args) {
    List<Integer> list = new ArrayList<Integer>();
    list.add(5);
    list.add(3);
    list.add(100);
    list.add(40);
    list.add(2);

    Comparator<Integer> comparator = new Comparator<Integer>() {
      @Override
      public int compare(Integer o1, Integer o2) {
        return Integer.valueOf(o1).compareTo(Integer.valueOf(o2));
      }
    };

   /* ラムダ式なら次のように定義する
    Comparator<Integer> comparator = (o1, o2) -> Integer.ValueOf(o1).compareTo(Integer.valueOf(o2);
   */

    Collections.sort(list, comparator);

    System.out.println(list);
  }
}
◆試験におけるポイント◆
ArrayListはスレッドセーフではないため、要素数だけfor文を回し、内部でadd or removeメソッドを使うと途中で要素数が変動して実行時例外になる。
・List.ofメソッドで生成したインスタンスは変更不可リストなのでadd、remove、clear、sort等のメソッドを実行するとはUnsupportedOperationExceptionになる。
・ListインターフェースのforEachメソッドはComsumer型のインターフェースを引数に指定し、要素数だけConsumer.acceptメソッドを実行。

まとめ

後半になるにしたがって文字数が多くなってしまいました。
特にラムダ式APIのCollectionやComparatorクラスは、Java Silverの試験範囲では複雑度が一段上なので、過去に受験者では「この領域は捨てる!」と判断した方もいるようです(実際、出題割合はそこまで多くないので捨ててもギリギリ受かるかも)。

今回この記事で整理したことで私自身は上手く理解できたかなと思っています。
この調子でJava Goldも合格します!その時にまた今回の様な記事を載せます。

(2022年9月7日追記)
Java Goldも合格しました!その勉強法、出題範囲のまとめはこちらをご参照ください。