コンストラクタとメソッドのオーバーロード
まえがき
chat-gptで作った学習ロードマップをプログラミング初学者が勉強する試みです
ロードマップはchat-gptを使用してますが、学習は公式のチュートリアルや技術系ブログなどを参考にしています
Goの復習も兼ねているのでGoとの違いについても言及します
今回はコンストラクタとメソッドのオーバーロードについて内容をまとめていきます
コンストラクタとは?
コンストラクタはクラスのインスタンスを初期化するときに呼び出されるメンバー
以下のふたつを両方満たすものはコンストラクタとして使える
- 返り値を持たない
- クラス名と同じメソッド
class クラス名 {
// コンストラクタ
クラス名() {
}
}
定義の通り返り値は持てないが引数はとることはできる
メンバー内にコンストラクタが無い場合はスーパークラスに引数の無いコンストラクタを探しにいく
- スーパークラスが無い場合
全てのクラスはObjectを継承してるのでObjectクラスから引数の無いコンストラクタを呼び出す(デフォルトのコンストラクタ) - スーパークラスが有る場合
スーパークラスに引数を持つコンストラクタしか存在しない場合はデフォルトコンストラクタの呼び出しが行われないためコンパイルエラー
2の場合はスーパークラスを確認しないとコンパイルエラーを起こすので注意
基本的な使い方
class Sum {
// フィールド
int i1;
int i2;
// コンストラクタ
Sum(int left, int right) {
i1 = left;
i2 = right;
}
}
public class Main() {
public static void Main(String[] args) {
// コンストラクタを利用して初期化
Sum sum1 = new Sum(5, 10);
}
}
明示的にコンストラクタを使う場合はフィールドの設定が簡潔に記述できたりする
メソッドのオーバーロードとは?
引数の数や型が違う、同じ名前のメソッドを複数定義すること
同じ名前なので管理しやすく、それでいて柔軟性を持たせることができる
オーバーロードの使い方
例えばふたつのprintメソッドを用意する
- 片方はintを引数に持つ
- もう片方はStringを引数に持つ
これはprintメソッドをオーバーロードしたことになり、同じprintメソッドでint型とString型のどちらでも引数に渡すことができる
public class Main {
// int型の引数を持つメソッド
static void print(int left, int right) {
System.out.print(left + right);
}
// String型の引数を持つメソッド
static void print(String left, String right) {
System.out.print(Integer.valueOf(left) + Integer.valueOf(right));
}
public static void main(String[] args) {
print(5, 10); // 15
print("5", "10"); // 15
}
}
printメソッドが複数あっても実行できる
引数の型と数が同じ場合はコンパイルエラー
Object型のような上位の型がオーバーロードしている場合は、intなどのより限定的な型が優先される
JavaとGoの違い
Goのコンストラクタ
Goでコンストラクタを利用する場合は関数を定義する
関数名は New+構造体名 とするのが慣習
type Numbers struct {
i1 int
i2 int
}
func NewNumbers(left, right int) *Numbers {
return &Numbers {
i1: left,
i2: right,
}
}
func main() {
n := NewNumbers(5, 10)
fmt.Print(n) // &{5, 10}
}
Goの場合は返り値にオブジェクトを定義する
構造体の利用目的を考えると上記のようにポインタを返すことが多い(メソッドを定義するときにポインタレシーバを使うため)
Goのオーバーロード
Goでオーバーロードはできない
なので他の方法で似たような機能を実装する
いくつか方法はあるがここでは以下のふたつを紹介する
- ジェネリクス+タイプスイッチ
- インターフェース+メソッド
1. ジェネリクス+タイプスイッチ
func print[T int | string](values... T) {
var result int
for _, value := range values {
switch v := any(value).(type) {
case int:
result += v
case string:
strNum, _ := strconv.Atoi(v)
result += strNum
}
}
fmt.Println(result)
}
func main() {
print(5, 10) // 15
print("8", "2") // 10
}
かなりめんどくさい
ジェネリクスで[]T型にintかstringのみを許可して引数にとる
範囲の繰り返し毎にany型にキャストしてタイプスイッチで型を判別する
=>intならresultに加算
=>stringならstrconvパッケージのAtoi関数を使ってintに変換してからresultに加算
最後にresultを出力
float64型とか受け入れる型が多い場合はジェネリクスじゃなくて素直にinterface{}型を引数にとった方が楽
2. インターフェース+メソッド
type myInter interface {
print()
}
type Integer []int
func (i Integer)print() {
var result int
for _, v := range i {
result += int(v)
}
fmt.Print(result)
}
type String []string
func (s String)print() {
var result int
for _, v := range s {
strNum, _ := strconv.Atoi(v)
result += strNum
}
fmt.Print(result)
}
func main() {
i := Integer{5, 10}
i.print()
s := String{"2", "8"}
myInter.print(s)
}
コード量が増えているがメソッドの数が増えればこちらの方がコード量が少なくなる(はず)
type キーワードを使って独自の型を実装して、それをもとにメソッドを作る
型が違えば同じメソッド名が使えるので要件の数だけ用意する
あとはメソッドの呼び出しをすれば同じようなことは実現できる
最後の myInter.print(s) はメソッド式の応用、あまり使わないと思うけど一応こういう使い方もできる
まとめ
コンストラクタもオーバーロードも直感的に使えて便利
オブジェクト指向の優秀さがわかる章だと感じた
コンストラクタの挙動など調べていて思ったが、こういう細かい仕様を調べるのは結構好きかもしれない
Discussion