なぜ Go 言語にコレクションクラスがないんだ!そうだ。 AI に作らせよう。
という訳で、Gemini だけで ArrayList をGo で作らせてみました。
そのときの内容を6回に分けてお伝えしていこうと思います。
- 連載リンク
- 第一回 まずは作ってもらった (このページ)
- 第二回 制約つきの構造体を追加してみた
- 第三回 Collection で定義されているメソッドを追加してみた
- 第四回 List で定義されているメソッドを追加してみた
- 第五回 Sort もしたい
- 第六回 Geminiとの対話で生まれたコードとまとめ
第一回 まずは作ってもらった
分割して掲載する都合上、実際に質問を投げた順番と掲載の順番とを少々入れ替えてあります。
XD.GROWTHで私が担当しているアプリケーションはGo言語で作られているのですが、
標準のスライスやマップの操作になかなか慣れることができず、
例えばJavaのようなコレクションやストリームのように
扱うことのできるものがほしかったので、
必要なメソッドだけ備えたライブラリを自作して運用しています。
このライブラリのメンテナンスに意外と時間を取られてしまうこともあるので、
今回はそれをAIに作ってもらうことにしました。
質問
ほぼ丸投げで質問を投げてみました。
Go言語でJavaのArrayListと完全に互換性のある構造体を作ってください
回答
承知いたしました。Go言語でJavaの
ArrayListと完全に互換性のある構造体を直接作成することはできません。Go言語とJavaの
ArrayListの根本的な違いJavaの
ArrayListは、オブジェクト指向の継承やインターフェース、ジェネリクスを多用しており、実行時に要素の型安全性を保証しつつ、動的なサイズ変更が可能です。一方、Go言語はシンプルさを重視した設計思想を持ち、ジェネリクスが比較的新しく導入されたこともあり、Javaのような完全なオブジェクト指向の継承モデルはありません。また、Go言語のスライスは、
ArrayListと同様の動的配列の機能を提供しますが、完全に互換性があるわけではありません。Go言語で同等の機能を持つ構造体を作成するアプローチ
ArrayListの主要な機能 (要素の追加、削除、取得、サイズ取得など) をGo言語で模倣するには、スライスとジェネリクスを組み合わせて、汎用的な動的配列を実装することになります。以下に、JavaのArrayListと似た機能を提供するGo言語の構造体の例を示します。
package main import ( "errors" "fmt" ) // MyArrayList はJavaのArrayListに似た機能を提供するジェネリックな動的配列です。 type MyArrayList[T any] struct { elements []T size int } // NewMyArrayList は新しいMyArrayListを初期化して返します。 func NewMyArrayList[T any]() *MyArrayList[T] { return &MyArrayList[T]{ elements: make([]T, 0), size: 0, } } // Add はリストの末尾に指定された要素を追加します。 func (list *MyArrayList[T]) Add(element T) { list.elements = append(list.elements, element) list.size++ } // Get はリスト内の指定された位置にある要素を返します。 // インデックスが範囲外の場合はエラーを返します。 func (list *MyArrayList[T]) Get(index int) (T, error) { if index < 0 || index >= list.size { var zeroValue T // Tのゼロ値 return zeroValue, errors.New("IndexOutOfBoundsException: Index " + fmt.Sprint(index) + " out of bounds for length " + fmt.Sprint(list.size)) } return list.elements[index], nil } // Remove はリスト内の指定された位置にある要素を削除します。 // 削除された要素を返し、リストのサイズを小さくします。 // インデックスが範囲外の場合はエラーを返します。 func (list *MyArrayList[T]) Remove(index int) (T, error) { if index < 0 || index >= list.size { var zeroValue T return zeroValue, errors.New("IndexOutOfBoundsException: Index " + fmt.Sprint(index) + " out of bounds for length " + fmt.Sprint(list.size)) } removedElement := list.elements[index] list.elements = append(list.elements[:index], list.elements[index+1:]...) list.size-- return removedElement, nil } // Size はリスト内の要素の数を返します。 func (list *MyArrayList[T]) Size() int { return list.size } // IsEmpty はリストに要素が含まれていない場合にtrueを返します。 func (list *MyArrayList[T]) IsEmpty() bool { return list.size == 0 } // Clear はリストからすべての要素を削除します。 func (list *MyArrayList[T]) Clear() { list.elements = make([]T, 0) list.size = 0 } // String はMyArrayListの文字列表現を返します。 func (list *MyArrayList[T]) String() string { return fmt.Sprintf("%v", list.elements) } func main() { // 文字列型のMyArrayListの例 stringList := NewMyArrayList[string]() stringList.Add("apple") stringList.Add("banana") stringList.Add("cherry") fmt.Println("String List:", stringList) // [apple banana cherry] fmt.Println("Size:", stringList.Size()) // 3 val, err := stringList.Get(1) if err == nil { fmt.Println("Get(1):", val) // banana } else { fmt.Println("Error:", err) } removed, err := stringList.Remove(0) if err == nil { fmt.Println("Removed:", removed) // apple } else { fmt.Println("Error:", err) } fmt.Println("String List after removal:", stringList) // [banana cherry] fmt.Println("Size after removal:", stringList.Size()) // 2 // int型のMyArrayListの例 intList := NewMyArrayList[int]() intList.Add(10) intList.Add(20) fmt.Println("Int List:", intList) // [10 20] fmt.Println("Size:", intList.Size()) // 2 // 範囲外アクセス _, err = stringList.Get(10) if err != nil { fmt.Println("Error accessing out of bounds:", err) // Error accessing out of bounds: IndexOutOfBoundsException: Index 10 out of bounds for length 2 } }解説
MyArrayList[T any]: Go 1.18で導入されたジェネリクスを使用しています。これにより、Tという型パラメータを使って、任意の型の要素を格納できる動的な配列を作成できます。anyは任意の型を表します。elements []T: 内部的にはGo言語のスライスを使用しており、これが動的な配列の役割を果たします。size int: 現在リストに格納されている要素の数を保持します。JavaのArrayListにおけるsize()メソッドに対応します。- メソッド:
Add(element T): 要素をリストの末尾に追加します。append関数を利用しています。Get(index int) (T, error): 指定されたインデックスの要素を取得します。範囲外アクセスの場合にはerrorを返します。Remove(index int) (T, error): 指定されたインデックスの要素を削除します。Goのスライス操作を使って要素を削除し、サイズを更新します。削除された要素とエラーを返します。Size() int: 現在の要素数を返します。IsEmpty() bool: リストが空かどうかを返します。Clear(): リストからすべての要素を削除し、内部のスライスを再初期化します。String():fmt.Stringerインターフェースを実装しており、fmt.Printlnなどで構造体を出力した際に内容が表示されるようにします。制限事項と考慮事項
- 完全な互換性ではない: 上述の通り、Go言語とJavaの言語設計思想の違いから、Javaの
ArrayListが持つすべての細かな挙動 (例:trimToSize(),ensureCapacity(), 複雑なイテレータのサポート、コレク> ションフレームワークとの連携など)を完全に模倣することは困難です。- エラーハンドリング: Javaでは例外 (
IndexOutOfBoundsExceptionなど)をスローしますが、Go言語では慣習的にエラーを返す形になります。上記の例でもエラーを返すように実装しています。- パフォーマンス: 内部的にはスライス操作が行われます。特に
Remove操作は、要素の移動が発生するため、大規模なリストで頻繁に中央の要素を削除するとパフォーマンスに影響を与える可能性があります。この
MyArrayListは、Go言語のイディオムに則りながら、JavaのArrayListが提供する主要な「動的配列」としての機能を提供するものです。
感想
まさかたった一言でここまでのものが出てくるとは思っていませんでした。
プログラミング言語の参考書のような使用例や解説付きで回答がくるのは感動しました。
size フィールド
先ほど作られたソースをぱっと見て気づいたのは、
スライスを使っているのにMyArrayList構造体にsizeフィールドが定義されていたこと。
確かに、ArrayListではsizeフィールドを持っていて、
配列のどこまで使っているかを持っていますが、
Goのスライスはそもそも配列へのポインタとサイズで構成されているものなので不要です。
質問
以下のように質問してみました。
sizeフィールドはlen()を使ってください
回答
承知いたしました。
sizeフィールドを持たず、len()関数を使って現在の要素数を取得するように変更します。Go言語のスライスは、len()関数で現在の要素数を取得するのがイディオムであり、sizeフィールドを別途持つ必要はありません。以下に修正したコードを示します。
package main import ( "errors" "fmt" ) // MyArrayList はJavaのArrayListに似た機能を提供するジェネリックな動的配列です。 type MyArrayList[T any] struct { elements []T // size int // sizeフィールドは不要 } // NewMyArrayList は新しいMyArrayListを初期化して返します。 func NewMyArrayList[T any]() *MyArrayList[T] { return &MyArrayList[T]{ elements: make([]T, 0), } } // Add はリストの末尾に指定された要素を追加します。 func (list *MyArrayList[T]) Add(element T) { list.elements = append(list.elements, element) } // Get はリスト内の指定された位置にある要素を返します。 // インデックスが範囲外の場合はエラーを返します。 func (list *MyArrayList[T]) Get(index int) (T, error) { if index < 0 || index >= len(list.elements) { // len()を使用 var zeroValue T // Tのゼロ値 return zeroValue, errors.New("IndexOutOfBoundsException: Index " + fmt.Sprint(index) + " out of bounds for length " + fmt.Sprint(len(list.elements))) } return list.elements[index], nil } // Remove はリスト内の指定された位置にある要素を削除します。 // 削除された要素を返し、リストのサイズを小さくします。 // インデックスが範囲外の場合はエラーを返します。 func (list *MyArrayList[T]) Remove(index int) (T, error) { if index < 0 || index >= len(list.elements) { // len()を使用 var zeroValue T return zeroValue, errors.New("IndexOutOfBoundsException: Index " + fmt.Sprint(index) + " out of bounds for length " + fmt.Sprint(len(list.elements))) } removedElement := list.elements[index] list.elements = append(list.elements[:index], list.elements[index+1:]...) return removedElement, nil } // Size はリスト内の要素の数を返します。 func (list *MyArrayList[T]) Size() int { return len(list.elements) // len()を使用 } // IsEmpty はリストに要素が含まれていない場合にtrueを返します。 func (list *MyArrayList[T]) IsEmpty() bool { return len(list.elements) == 0 // len()を使用 } // Clear はリストからすべての要素を削除します。 func (list *MyArrayList[T]) Clear() { list.elements = make([]T, 0) } // String はMyArrayListの文字列表現を返します。 func (list *MyArrayList[T]) String() string { return fmt.Sprintf("%v", list.elements) } func main() { // 文字列型のMyArrayListの例 stringList := NewMyArrayList[string]() stringList.Add("apple") stringList.Add("banana") stringList.Add("cherry") fmt.Println("String List:", stringList) fmt.Println("Size:", stringList.Size()) // Size()メソッドはlen()を使用 val, err := stringList.Get(1) if err == nil { fmt.Println("Get(1):", val) } else { fmt.Println("Error:", err) } removed, err := stringList.Remove(0) if err == nil { fmt.Println("Removed:", removed) } else { fmt.Println("Error:", err) } fmt.Println("String List after removal:", stringList) fmt.Println("Size after removal:", stringList.Size()) // Size()メソッドはlen()を使用 // int型のMyArrayListの例 intList := NewMyArrayList[int]() intList.Add(10) intList.Add(20) fmt.Println("Int List:", intList) fmt.Println("Size:", intList.Size()) // 範囲外アクセス _, err = stringList.Get(10) if err != nil { fmt.Println("Error accessing out of bounds:", err) } }変更点
MyArrayList構造体からsize intフィールドを削除しました。Addメソッドからlist.size++を削除しました。Removeメソッドからlist.size--を削除しました。Get,Remove,Size,IsEmptyメソッドでlist.sizeの代わりにlen(list.elements)を直接使用するように変更しました。Clearメソッドではlist.elements = make([]T, 0)のみでよく、list.size = 0は不要になりました。これにより、Go言語のスライスのイディオムに沿った、より簡潔な実装になりました。
感想
すばらしい!
まとめ
でもやっぱり、完璧 (=頭の中に思い描いた通り) なものは出てくるはずもなく、
次回からは質問を重ね、頭の中に思い描いたArrayListへと近づけていきたいと思います。