クラス(class)はオブジェクト指向プログラミングの基本的な仕組みを提供するもので、オブジェクト(インスタンス とも言います)が持つ、フィールド(属性、アトリビュート、プロパティ とも呼ばれます)や、メソッド(関数、ファンクション と呼ばれることもあります)などを定義します。
class TestClass { TypeName FieldName; TypeName MedhotdName(Args) { ... } }
あるクラスのインスタンスを作成するには、new を用います。例えば、TestClass という名前のクラスのインスタンスを作るには次のようにします。
TestClass o = new TestClass();
例えば下記の例では、Person という名前のクラスを定義しています。Person クラスは、myName と myAge というフィールドを持っています。SetName()、GetName()、SetAge()、GetAge() というメソッドを持っています。
class Person { String myName; int myAge; public void SetName(String name) { myName = name; } public String GetName() { return myName; } public void SetAge(int age) { myAge = age; } public int GetAge() { return myAge; } }
ここで作成した Person クラスを使用するには次のようにします。まず、new でインスタンス tanaka や suzuki を作成し、そのメソッドを呼び出しています。
class PersonTest { public static void main(String[] args) { Person tanaka = new Person(); // 田中さんオブジェクトを作る tanaka.SetName("Tanaka"); // 田中さんの名前を設定する tanaka.SetAge(26); // 田中さんの年齢を設定する Person suzuki = new Person(); // 鈴木さんオブジェクトを作る suzuki.SetName("Suzuki"); // 鈴木さんの名前を設定する suzuki.SetAge(32); // 鈴木さんの年齢を設定する System.out.println(tanaka.GetName()); // 田中さんの名前を参照する System.out.println(tanaka.GetAge()); // 田中さんの年齢を参照する System.out.println(suzuki.GetName()); // 鈴木さんの名前を参照する System.out.println(suzuki.GetAge()); // 鈴木さんの年齢を参照する } }
クラス、インスタンス、フィールド、メソッドについてまとめてみます。
用語 | 別の呼び方 | サンプルでの具体例 |
---|---|---|
クラス | Person | |
フィールド | 属性、アトリビュート、プロパティ | myName、myAge |
メソッド | ファンクション、関数 | SetName()、GetName()、SetAge()、GetAge() |
インスタンス | オブジェクト | tanaka、suzuki |
コンストラクタ は、クラス名と同じ名前を持つ特別なメソッドです。インスタンスが生成された時に自動的に呼ばれ、インスタンスの初期化処理を行います。
class Person { String myName; Person(String myName) { this.myName = myName; } }
コンストラクタには上記のように引数を持つことができます。下記の例では、Person クラスのインスタンス tanaka を生成する際に、コンストラクタの処理を利用して名前(myName)の設定を行っています。
Person tanaka = new Person("tanaka");
デストラクタ は、クラスが消滅する際に自動的に呼ばれる特別なメソッドで、クラスの後始末処理を記述します。C++言語などではサポートされていますが、Java ではすべての破棄作業は自動的に行われるという思想の元、デストラクタは不要と考えられています。
クラス内部に定義するクラスを インナークラス(内部クラス)と呼びます。
class OuterClass { class InnerClass { } } public class Main { public static void main(String[] args) { OuterClass o = new OuterClass(); OuterClass.InnerClass o2 = o.new InnerClass(); } }
static をつけて使用することが多いかと思います。
class OuterClass { static class InnerClass { } } public class Main { public static void main(String[] args) { OuterClass.InnerClass o2 = new OuterClass.InnerClass(); } }
クラスの修飾子には、public、final、abstract、strictfp を指定することができます。
修飾子 class クラス名 { : }
インナークラス(クラス内部に定義するクラス)には加えて、protected、private、static を指定することが可能になります。
class クラス名 { 修飾子 class クラス名 { : } }
クラスは親子関係を持ち、親クラス (スーパークラス) を 子クラス (サブクラス)が 継承 することができます。継承するにはサブクラスを定義する際に extends を使用します。サブクラスはスーパークラスのフィールドやメソッドをそのまま引き継ぐことができます。
class SuperClass { // 親クラス void method1() { System.out.println("method1"); } } class SubClass extends SuperClass { // 子クラス void method2() { System.out.println("method2"); } } public class Main { public static void main(String[] args) { SubClass o = new SubClass(); o.method1(); // 親クラスのメソッドも呼び出せる o.method2(); // 子クラスのメソッドも呼び出せる } }
super は親クラスを意味します。親クラスのコンストラクタは、引数を持たない場合は子クラス作成時に暗黙的に呼び出されますが、引数を持つ場合は暗黙的に呼び出されないため、下記の様に明示的に呼び出す必要があります。
class SuperClass { String name; SuperClass(String name) { this.name = name; } void printName() { System.out.println(this.name); } } class SubClass extends SuperClass { SubClass(String name) { super(name); // 親クラスのコンストラクタを呼び出す System.out.println(super.name); // 親クラスのフィールドを参照する super.printName(); // 親クラスのメソッドを呼び出す } }
instanceof 演算子は、オブジェクト(インスタンス)が、指定したクラスまたはその上位のクラスに属しているかどうかを調べます。
SubClass o = new SubClass(); if (o instanceof SuperClass) { System.out.println("属してます"); }
型の情報を引数の様に渡すことで、未定の型に対するクラスを定義することができます。これを ジェネリック と呼びます。下記の例では Test というクラスは T という型引数を取り、T の型によってフィールドの型などを柔軟に変更することができます。
class Test<T> { T value; Test(T value) { this.value = value; } } public class Main { public static void main(String[] args) { Test<String>t1 = new Test<String>("ABC"); Test<Integer>t2 = new Test<Integer>(123); System.out.println(t1.value); System.out.println(t2.value); } }
よく使用される例は、Integer のリスト、String のリストなどを扱うパターンです。
import java.util.List; import java.util.ArrayList; public class Main { public static void main(String[] args) { List<String>list = new ArrayList<String>(); list.add("AAA"); list.add("BBB"); list.add("CCC"); System.out.println(list.get(0)); System.out.println(list.get(1)); System.out.println(list.get(2)); } }
ジェネリックを利用して任意の型のパラメータを引数として受け取るメソッドも定義できます。
public class Main { static <T> String getType(T x) { return ((Object)x).getClass().getSimpleName(); } public static void main(String[] args) { String s = "ABC"; System.out.println(getType(123)); // Integer System.out.println(getType(12.3)); // Double System.out.println(getType("ABC")); // String } }
Java 7 からは右辺の型を省略できるようになりました。
List<String>list = new ArrayList<>();
Java 16 でサポートされた機能で、下記の特徴を持つクラスを生成します。
通常のクラスを用いると下記のような定義になります。
final class Book { private final String title; // タイトル private final String author; // 著者 public Book(String title, String author) { // コンストラクタ this.title = title; this.author = author; } public String title() { // アクセッサ return title; } public String author() { // アクセッサ return author; } @Override public String toString() { // toString()メソッド return "Book [title=" + title + ", author=" + author + "]"; } @Override public boolean equals(Object o) { // equals()メソッド return (this.title == ((Book)o).title) && (this.author == ((Book)o).author); } @Override public int hashCode() { // hashCode()メソッド return super.hashCode(); } } public class Main { public static void main(String[] args) { Book b1 = new Book("I Am a Cat.", "Soseki Natsume"); Book b2 = new Book("Night On The Milky Way Train", "Kennji Miyazawa"); System.out.println(b1.title()); System.out.println(b1.author()); System.out.println(b1.equals(b2)); System.out.println(b1.toString()); System.out.println(b1.hashCode()); } }
これを、Java 16 でサポートされた レコードクラス で実装すると下記だけで実装できます。
record Book (String title, String author) { }
Java 8 で ラムダ式 がサポートされました。下記は、run() という抽象メソッドを持つ Runnable インタフェースを実装するローカルクラス LocalClass を定義し、そのインスタンスを作成して、run() を実行するサンプルです。
public static void main(String[] args) { class LocalClass implements Runnable { @Override public void run() { System.out.println("Hello!"); } } Runnable r = new LocalClass(); r.run(); }
同じ処理を、ラムダ式を用いて下記の様に記述することができます。
public static void main(String[] args) { Runnable r = () -> { System.out.println("Hello!"); }; r.run(); }引数は複数記述することもできます。引数がひとつの場合は括弧を省略できます。実行文が単一の場合は { return } を省略することができます。
(arg1, arg2) -> { statements; return expr; } (arg1) -> { return expr; } arg1 -> expr
Java のラムダ式は、他の言語のラムダ式とは少し異なっていて、「単一の抽象メソッドを持つインタフェースを実装した無名クラスのインスタンスを生成する式」という位置づけになります。
Java 8 ではラムダ式と合わせて、関数型インタフェース がサポートされました。関数型インタフェースは「抽象メソッドをひとつのみ持つインタフェース」です。@FunctionalInterface は省略可能ですが、指定しておくと関数型インタフェースの条件を満たさない場合にコンパイルエラーにすることができます。
@FunctionalInterface interface MyFunctionalInterface { public abstract int run(String arg); }
java.util.function 配下に下記などの関数型インタフェースがあらかじめ定義されています。
Function<T,R> ... T型の引数を受け取り、R型の戻り値を返す。メソッドは R apply(T)。 BiFunction<T,U,R> ... T型,U型の2つの引数を受け取り、R型の戻り値を返す。メソッドは R apply(T, U)。 UnaryOperator<T> ... T型の引数を1つ受け取り、T型の戻り値を返す。メソッドは T apply(T)。 BinaryOperator<T> ... T型の引数を2つうけとり、T型の戻り値を返す。メソッドは T apply(T, T)。 Consumer<T> ... T型の引数を受け取り、値を返さない。メソッドは void accept(T)。 BiConsumer<T,U> ... T型,U型の2つの引数を受け取り、値を返さない。メソッドは void accept(T, U)。 Predicate<T> ... T型の引数を受け取り、真偽を返す。メソッドは boolean test(T)。 BiPredicate<T,U> ... T型,U型の2つの引数を受け取り、真偽を返す。メソッドは boolean test(T, U)。 Supplier<R> ... 引数を受け取らず、R型の戻り値を返す。メソッドは R get()。
下記に使用例を示します。
import java.util.function.*; public class Main { public static void main(String[] args) { Function<String, Integer> strLength = (str) -> { return str.length(); }; int len = strLength.apply("ABC"); System.out.println(len); BiFunction<String, Integer, Character> charAt = (str, n) -> { return str.charAt(n); }; char c = charAt.apply("ABCDEFG", 3); System.out.println(c); BiPredicate<String, String> strEqual = (str1, str2) -> { return str1 == str2; }; boolean b = strEqual.test("ABC", "ABC"); System.out.println(b); } }
上記だけだとあまりメリットが感じられませんが、例えば、Stream API では「リストの中から80点以上のものを抽出する」といった処理をラムダ式を用いることで簡単に記述できるようになっています。
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Integer> points = List.of(85, 62, 56, 93, 72, 45, 89);
points.stream()
.filter(point -> point >= 80) // ラムダ式
.forEach(System.out::println);
}
}