
みなさまこんにちは〜!
メモリアインクのすだです。
Androidアプリを開発していく中で、「抽象クラス」という機能を使う時があると思います。きちんとした理解を持って使用すれば、とても便利な機能なので 今日は一緒に学んでいきましょう。
この記事を読んでわかること…
・抽象クラスとは?
・Kotlinでの開発における抽象クラスの使い方
・インターフェースとの併用方法
環境
- Kotlin (ver 1.9.0)
- Android Studio (Giraffe | 2022.3.1 Patch 3)
抽象クラスとは?
抽象クラスは「中身が未完成なクラス」のことで、
「一部の処理は共通で持つけど、ある部分は子クラスで必ず書いてね」というクラスです。
中身を持った関数も、持たない関数(abstract)も書けますが、自分では使わない=つまりはインスタンス化できません。
必ず継承される前提で作ります。
抽象クラスは、
・共通レイアウトや処理がある複数のActivityやFragmentを管理したいとき
・API通信前後のローディング表示など、共通のUIフローを統一したいとき
・各画面で一部だけ処理を変えたいとき(テンプレートメソッドパターン)
などのケースが向いていると言えるでしょう。
抽象クラスの基本的な使い方
class
の宣言時に先頭に abstract
をつけることで、継承を前提とした「抽象クラス」を定義できます。
抽象クラスには、中身のない関数(= abstract関数)を定義できるため、子クラスで必ず実装させることができます。▼
abstract class BaseScreen {
// 抽象関数:子クラスで必ず実装が必要
abstract fun loadData()
// 具体的な処理:すでに完成していて、子クラスはそのまま使える
fun showLoading() {
println("読み込み中...")
}
fun hideLoading() {
println("読み込み完了")
}
}
そして、子クラスで継承したい場合は
宣言した子クラス名横に :
(コロン)つなぎでクラス名を書き、
関数はoverrideを書いて以下のように上書きします。▼
class MainActivity : BaseScreen() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
showLoading() // 親で定義された共通処理
loadData() // 自分で書く処理
hideLoading() // 共通処理
}
override fun loadData() { // 上書き
println("メイン画面のデータを読み込んでいます")
}
}
抽象クラスの特徴
①インスタンス化できない(直接 new して使えない)
抽象クラスは未完成なクラスなので、そのままでは使えません。必ず子クラスで継承して使います。
abstract class Animal {
abstract fun speak()
}
// val a = Animal() ← × エラー。インスタンス化できない
②コンストラクタを持てる
コンストラクタ機能を使えるので、「状態の保持」や「初期化処理」が可能です。
abstract class User(val name: String) {
abstract fun greet()
}
class JapaneseUser(name: String) : User(name) {
override fun greet() {
println("こんにちは、$name さん!")
}
}
class EnglishUser(name: String) : User(name) {
override fun greet() {
println("Hello, $name!")
}
}
共通のプロパティ(name)を持ちながら、あいさつの内容(greet)だけは子クラスごとに変えたい といった仕組みを作ることができます。
子クラス自身でも名前を受け取るかつ、その名前を親の User に渡す(コンストラクタ呼び出し)と言うイメージです。
③クラスは 1つしか継承できない(単一継承)
Kotlinでは、抽象クラスに限らずクラスは1つだけしか継承できません。
共通の処理は「抽象クラス」、複数の機能は「インターフェース」で表現するのがよいでしょう。
abstract class A
abstract class B
// class C : A(), B() ← × これはできない
抽象クラスにおける関数宣言の違い
抽象クラスにおける関数の宣言は、基本的に以下3つの方法があります。
宣言方法 | 概要 | 子クラスでの対応 |
---|---|---|
fun | 通常の関数 | オーバーライド不可 |
open fun | 上書きしてもいい関数 | オーバーライド任意 |
abstract fun | 必ず上書きする関数 | オーバーライド必須 |
fun(オーバーライド不可)
fun
という通常の関数の書き方は、「子クラスに上書きさせたくない処理」ということになります。
共通処理として固定する場合に使うことが多いです。▼
fun showLoading() {
println("読み込み中...")
}
open fun(オーバーライド可)
open fun
は、「子クラスでオーバーライド(上書き)してもOK」という柔軟な設計にしたい場合に使います。
共通の処理をデフォルトとして用意しつつ、必要に応じて子クラス側で変更できるというのがポイントです。
たとえば、通常は共通の動作を使い、特定の画面だけ独自の処理にしたいときなどに便利です。▼
open fun showTitle() {
println("共通タイトルを表示")
}
abstract fun(オーバーライド必須)
abstract fun
は、関数の処理を親(抽象クラス)では定義せず、子クラスで必ずオーバーライドして実装する必要がある関数です。
中身(処理の内容)は抽象クラスでは書かず、継承したクラス側で自由に定義します。▼
abstract fun loadData()
たとえば、「処理の流れは同じだが、中身はそれぞれ違う」といったような処理を作りたい場合に便利です。
例)APIレスポンスに対する「変換処理」や「結果の処理」が異なるとき
abstract class ApiHandler {
abstract fun handleResult(response: String)
}
class LoginApiHandler : ApiHandler() {
override fun handleResult(response: String) {
// ログイン成功後の処理
}
}
インターフェースと組み合わせることもできる
インターフェースと抽象クラスを組み合わせて使うのはよくある&とても効果的な設計手法です。
Androidアプリ開発やアーキテクチャ設計でも、役割を分けて併用することで「拡張性」「可読性」「保守性」が一気に上がります。
インターフェースについては、以下の記事で詳しくご紹介しておりますので、ぜひ合わせてご覧ください!
よくある組み合わせパターン①
たとえば 以下のような構成で処理を作ってみます。
interface Notifier ← "通知を送る" ルール定義
↑
abstract class BaseNotifier ← 共通処理(ログ出力など)
↑
class EmailNotifier ← 実際の通知方法を実装
↓
MainActivity で使う
↓
Step 1:インターフェースの定義(Notifier.kt)
interface Notifier {
fun notifyUser(message: String)
}
ここではまだ処理の中身は書きません。
Step 2:抽象クラス(BaseNotifier.kt)
abstract class BaseNotifier : Notifier {
fun log(message: String) {
println("ログ: $message")
}
}
ここでインターフェースNotifierを継承することで、
「Notifier に定められたルール(= notifyUser関数)を持っているべき」という約束(契約)を引き継いでいます。
また、ログを出力する共通処理をここにまとめておくことで、子クラスから log() を自由に呼び出すことができます。
Step 3:実装クラス(EmailNotifier.kt)
class EmailNotifier : BaseNotifier() {
override fun notifyUser(message: String) {
println("メール送信: $message")
log(message) // ← 共通処理を使ってログ出力
}
}
Notifier のルールを守りつつ、具体的な処理(メール送信)をここで書くきます。
さらに log()
を呼び出して、共通のログ出力も行います。
Step 4. Activity で使う
notifier = EmailNotifier() // ← 実装クラスを代入
button.setOnClickListener {
notifier.notifyUser("キャンペーンのお知らせ!")
}
よくある組み合わせパターン②
次は、より実践的で 商用アプリによく取り入れられるパターンを書いてみます。
たとえば、各画面(Activity)で「loadData()
」というデータ読み込み処理を必ず実装させたい。
共通の仕組みは親クラスに持たせて、個別の処理だけ子クラスに任せたいという処理にします。
interface Loadable {
fun loadData()
}
abstract class BaseActivity(private val screenTitle: String) : AppCompatActivity(), Loadable {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println("画面タイトル:$screenTitle")
loadData() // ← interfaceで定めたルールを呼び出し
}
}
この抽象クラス内でLoadable
を実装しているので、loadData()
を持っている前提です。
しかしまだここでは loadData()
の中身は書きません(つまり実行は子クラスに任せる)。onCreate()
の中で loadData()
を呼んでいる → 各画面は起動時にデータ読み込みされるという仕組みです。
これによって「どの画面でも、onCreateで自動的に loadData が動く」という処理が作れます。
class HomeActivity : BaseActivity("ホーム画面") {
override fun loadData() {
println("画面のデータ読み込み中…")
}
}
実行結果イメージ↓
画面タイトル:ホーム画面
ホーム画面のデータ読み込み中…
BaseActivity を継承することで、共通処理(onCreateの流れなど)をそのまま使えます。loadData()
の中身だけ自分で実装するようにします。
(この場合、上記子クラスで必ずloadData()
の処理を書かないとコンパイルエラーになります。)
まとめ
おつかれさまでした。いかがでしたでしょうか!
抽象クラスは、共通のプロパティや処理を持たせながら、一部の関数だけ子クラスに実装させたいときに使う強力な仕組みです。
ぜひ使いこなして、実装に役立ててください!



技術者としてのキャリアパスを次のレベルへと進めたい皆様、
<未経験からIT・Webエンジニアを目指すなら【ユニゾンキャリア】>
自分の市場価値をさらに向上させてみませんか?
それではまた次回の記事でお会いしましょう!
コメント