Groovy

#29
PYPL#23
GitHub#27
IEEESpectrum#23
プログラミング言語動的言語JVMスクリプト言語Java互換DSL

プログラミング言語

Groovy

概要

GroovyはJVM上で動作する動的プログラミング言語で、Javaとの高い互換性とより簡潔な構文を提供します。

詳細

Groovyは2003年にJames Strachan氏によって開発されたJVM上で動作する動的プログラミング言語で、Javaとの完全な互換性を保ちながら、より簡潔で表現力豊かな構文を提供します。静的型付けと動的型付けの両方をサポートし、関数型プログラミングとオブジェクト指向プログラミングの機能を兼ね備えています。Javaライブラリを直接利用でき、既存のJavaコードとシームレスに統合可能です。クロージャ、演算子オーバーロード、文字列補間、簡潔なコレクション操作など、開発者の生産性を向上させる多くの機能を提供します。Apache Gradleビルドツール、Jenkins CI/CD、Spring Frameworkなどで広く採用されており、スクリプティング、テスト自動化、DSL(Domain Specific Language)構築、Web開発などの分野で活用されています。

書き方の例

Hello World

// 基本的な出力
println "Hello, World!"

// 変数を使った出力
def message = "こんにちは、Groovy!"
println message

// 文字列補間
def name = "太郎"
def age = 25
println "私の名前は${name}で、${age}歳です。"

// GString(Groovy String)
def template = """
名前: $name
年齢: $age
説明: これはGroovyの文字列補間です。
"""
println template

// Javaスタイルの出力も可能
System.out.println("Java互換の出力")

変数とデータ型

// 動的型付け(def)
def dynamicVar = 42
dynamicVar = "文字列に変更"
dynamicVar = [1, 2, 3, 4, 5]

// 静的型付け
String staticString = "静的型付け文字列"
int staticInt = 100
boolean staticBoolean = true

// プリミティブ型
byte b = 1
short s = 100
int i = 1000
long l = 10000L
float f = 3.14f
double d = 3.14159
char c = 'A'

// リスト(動的な配列)
def list = [1, 2, 3, 4, 5]
def mixedList = [1, "文字列", true, 3.14]
def emptyList = []

// リストの操作
list << 6  // 要素追加
list += [7, 8, 9]  // 複数要素追加
println "リスト: $list"
println "最初の要素: ${list[0]}"
println "最後の要素: ${list[-1]}"

// マップ(連想配列)
def map = [name: "田中太郎", age: 30, city: "東京"]
def emptyMap = [:]

// マップの操作
map.job = "エンジニア"  // 新しいキー追加
map["hobby"] = "読書"   // ブラケット記法
println "マップ: $map"
println "名前: ${map.name}"

// 範囲(Range)
def range1 = 1..10     // 1から10まで(10を含む)
def range2 = 1..<10    // 1から10まで(10を含まない)
def charRange = 'a'..'z'

println "範囲1: $range1"
println "文字範囲: $charRange"

// 正規表現
def pattern = ~/\d+/   // Groovyの正規表現リテラル
def text = "私は25歳です"
def matcher = text =~ pattern
println "マッチした数値: ${matcher[0]}"

条件分岐

def score = 85

// 基本的なif文
if (score >= 90) {
    println "評価: A"
} else if (score >= 80) {
    println "評価: B"
} else if (score >= 70) {
    println "評価: C"
} else {
    println "評価: D"
}

// 三項演算子
def grade = score >= 80 ? "合格" : "不合格"
println "結果: $grade"

// エルビス演算子(null合体演算子)
def name = null
def displayName = name ?: "匿名"
println "表示名: $displayName"

// switch文(Groovyの拡張)
def value = "文字列"
switch (value) {
    case String:
        println "文字列型です"
        break
    case Integer:
        println "整数型です"
        break
    case List:
        println "リスト型です"
        break
    case ~/\d+/:  // 正規表現でのマッチ
        println "数値のパターンです"
        break
    default:
        println "その他の型です"
}

// switch文でのin演算子
def number = 5
switch (number) {
    case 1..5:
        println "1から5の範囲"
        break
    case [10, 15, 20]:
        println "10, 15, 20のいずれか"
        break
    default:
        println "その他"
}

// 安全なナビゲーション演算子
def person = null
println "名前: ${person?.name}"  // nullでもエラーにならない

// Groovy Truth(Groovyの真偽値判定)
if ("") println "空文字列は偽"        // 実行されない
if ("文字列") println "文字列は真"     // 実行される
if ([]) println "空リストは偽"        // 実行されない
if ([1, 2, 3]) println "リストは真"   // 実行される
if (0) println "0は偽"               // 実行されない
if (1) println "0以外は真"           // 実行される

コレクションと高階関数

// リストの高階関数
def numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

// each(forEach相当)
numbers.each { num ->
    println "数値: $num"
}

// eachWithIndex
numbers.eachWithIndex { num, index ->
    println "インデックス $index: $num"
}

// collect(map相当)
def squares = numbers.collect { it * it }
println "2乗: $squares"

// find(最初にマッチする要素)
def firstEven = numbers.find { it % 2 == 0 }
println "最初の偶数: $firstEven"

// findAll(filter相当)
def evenNumbers = numbers.findAll { it % 2 == 0 }
println "偶数: $evenNumbers"

// any(いずれかが条件を満たす)
def hasLargeNumber = numbers.any { it > 5 }
println "5より大きい数があるか: $hasLargeNumber"

// every(すべてが条件を満たす)
def allPositive = numbers.every { it > 0 }
println "すべて正の数か: $allPositive"

// sum
def total = numbers.sum()
println "合計: $total"

// min/max
println "最小値: ${numbers.min()}"
println "最大値: ${numbers.max()}"

// sort
def sorted = numbers.sort { a, b -> b <=> a }  // 降順
println "降順ソート: $sorted"

// groupBy
def grouped = numbers.groupBy { it % 2 == 0 ? "偶数" : "奇数" }
println "グループ化: $grouped"

// マップの操作
def grades = [太郎: 85, 花子: 92, 次郎: 78]

grades.each { name, grade ->
    println "$name: $grade"
}

def highGrades = grades.findAll { name, grade -> grade >= 80 }
println "高得点者: $highGrades"

// 文字列の操作
def text = "Hello, Groovy World!"
println "大文字: ${text.toUpperCase()}"
println "分割: ${text.split(' ')}"

// リスト内包表記風
def evenSquares = (1..10).findAll { it % 2 == 0 }.collect { it * it }
println "偶数の2乗: $evenSquares"

関数とクロージャ

// 基本的な関数(メソッド)定義
def greet(name) {
    return "こんにちは、${name}さん!"
}

// 戻り値の型指定
String greetTyped(String name) {
    "こんにちは、${name}さん!"  // returnは省略可能
}

// デフォルト引数
def calculateArea(width, height = 10) {
    width * height
}

// 可変長引数
def sum(... numbers) {
    numbers.sum()
}

// クロージャ(無名関数)
def square = { x -> x * x }
def add = { x, y -> x + y }

// 単一パラメータの場合は'it'を使用可能
def double = { it * 2 }

// クロージャを引数に取る関数
def applyOperation(List list, Closure operation) {
    list.collect(operation)
}

// 高階関数
def createMultiplier(factor) {
    return { number -> number * factor }
}

// 関数の使用例
println greet("山田")
println calculateArea(5)  // heightはデフォルト値
println sum(1, 2, 3, 4, 5)

println "2乗: ${square(5)}"
println "加算: ${add(10, 20)}"

def doubled = applyOperation([1, 2, 3, 4, 5], double)
println "2倍: $doubled"

def tripler = createMultiplier(3)
println "3倍: ${tripler(7)}"

// クロージャのデリゲート(delegate)
class Calculator {
    def add(a, b) { a + b }
    def multiply(a, b) { a * b }
}

def calc = new Calculator()
def operation = {
    def result1 = add(5, 3)
    def result2 = multiply(4, 6)
    [result1, result2]
}

operation.delegate = calc
operation.resolveStrategy = Closure.DELEGATE_FIRST
println "計算結果: ${operation()}"

// 再帰関数
def factorial
factorial = { n ->
    n <= 1 ? 1 : n * factorial(n - 1)
}

println "階乗: ${factorial(5)}"

// メモ化(Groovy 2.2+)
def fibonacci
fibonacci = { n ->
    n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2)
}.memoize()

println "フィボナッチ: ${fibonacci(10)}"

クラスとオブジェクト指向

// 基本的なクラス定義
class Person {
    String name
    int age
    private String id
    
    // コンストラクタ
    Person(String name, int age) {
        this.name = name
        this.age = age
        this.id = generateId()
    }
    
    // メソッド
    def greet() {
        "こんにちは、私は${name}です。"
    }
    
    def haveBirthday() {
        age++
        println "${name}は${age}歳になりました。"
    }
    
    private def generateId() {
        "ID_${System.currentTimeMillis()}"
    }
    
    // toString
    String toString() {
        "Person(name: $name, age: $age)"
    }
}

// 継承
abstract class Animal {
    String name
    
    Animal(String name) {
        this.name = name
    }
    
    abstract def makeSound()
    
    def introduce() {
        "私は${name}という動物です。"
    }
}

class Dog extends Animal {
    String breed
    
    Dog(String name, String breed) {
        super(name)
        this.breed = breed
    }
    
    def makeSound() {
        "ワンワン"
    }
    
    def fetch() {
        "${name}がボールを取ってきました。"
    }
}

// インターフェース
interface Flyable {
    def fly()
}

interface Swimmable {
    def swim()
}

// 複数インターフェースの実装
class Duck extends Animal implements Flyable, Swimmable {
    Duck(String name) {
        super(name)
    }
    
    def makeSound() {
        "クワックワッ"
    }
    
    def fly() {
        "${name}が空を飛んでいます"
    }
    
    def swim() {
        "${name}が池で泳いでいます"
    }
}

// トレイト(Groovy 2.3+)
trait Debuggable {
    def debug(message) {
        println "[DEBUG] ${this.class.simpleName}: $message"
    }
}

trait Timestamped {
    Date created = new Date()
    
    def getAge() {
        (System.currentTimeMillis() - created.time) / 1000
    }
}

// トレイトを使用するクラス
class Service implements Debuggable, Timestamped {
    String name
    
    Service(String name) {
        this.name = name
    }
    
    def doWork() {
        debug("作業を開始します: $name")
        // 何らかの処理
        debug("作業が完了しました")
    }
}

// Groovyのプロパティ(自動的にgetter/setterが生成される)
class Book {
    String title
    String author
    BigDecimal price
    
    // カスタムgetter
    String getDisplayTitle() {
        "「$title」"
    }
    
    // カスタムsetter
    void setPrice(BigDecimal price) {
        if (price < 0) {
            throw new IllegalArgumentException("価格は0以上である必要があります")
        }
        this.price = price
    }
}

// 使用例
def person = new Person("田中太郎", 30)
println person.greet()
person.haveBirthday()

def animals = [
    new Dog("ポチ", "柴犬"),
    new Duck("ドナルド")
]

animals.each { animal ->
    println animal.introduce()
    println "鳴き声: ${animal.makeSound()}"
    if (animal instanceof Flyable) {
        println animal.fly()
    }
}

def service = new Service("テストサービス")
service.doWork()
println "サービス作成からの経過時間: ${service.age}秒"

def book = new Book(title: "Groovy入門", author: "山田太郎", price: 2500)
println "本: ${book.displayTitle} by ${book.author}"

ファイル操作とI/O

// ファイルの読み書き
def fileName = "sample.txt"

// ファイルに書き込み
new File(fileName).text = """これはテストファイルです。
Groovyでファイル操作を学んでいます。
複数行のテキストを書き込めます。"""

// ファイルの読み込み
def content = new File(fileName).text
println "ファイル内容:\n$content"

// 行ごとの読み込み
new File(fileName).eachLine { line, lineNumber ->
    println "行 $lineNumber: $line"
}

// ファイルの存在確認と操作
def file = new File(fileName)
if (file.exists()) {
    println "ファイルサイズ: ${file.size()} バイト"
    println "最終更新日: ${new Date(file.lastModified())}"
}

// CSV形式のデータ処理
def csvData = """名前,年齢,職業
田中太郎,30,エンジニア
山田花子,25,デザイナー
佐藤次郎,35,マネージャー"""

new File("employees.csv").text = csvData

// CSVパーサーの簡単な実装
def employees = []
new File("employees.csv").eachLine { line, lineNumber ->
    if (lineNumber > 1) {  // ヘッダーをスキップ
        def (name, age, job) = line.split(',')
        employees << [name: name, age: age as Integer, job: job]
    }
}

employees.each { emp ->
    println "${emp.name} (${emp.age}歳) - ${emp.job}"
}

// ディレクトリの操作
def dir = new File(".")
dir.eachFile { file ->
    println "${file.name} (${file.isDirectory() ? 'ディレクトリ' : 'ファイル'})"
}

// Groovyの@Grab(依存関係の動的取得)
@Grab('org.apache.commons:commons-lang3:3.12.0')
import org.apache.commons.lang3.StringUtils

def text = "  Hello, World!  "
println "トリム後: '${StringUtils.strip(text)}'"

// JSON処理
@Grab('org.apache.groovy:groovy-json:4.0.0')
import groovy.json.*

def jsonData = [
    name: "APIレスポンス",
    data: [
        [id: 1, title: "項目1"],
        [id: 2, title: "項目2"]
    ],
    status: "成功"
]

def jsonString = JsonOutput.toJson(jsonData)
println "JSON形式:\n${JsonOutput.prettyPrint(jsonString)}"

def parsedJson = new JsonSlurper().parseText(jsonString)
println "パース後の名前: ${parsedJson.name}"

特徴的な機能

メタプログラミング

// 動的メソッド追加
String.metaClass.isPalindrome = {
    delegate == delegate.reverse()
}

println "'racecar'.isPalindrome(): ${'racecar'.isPalindrome()}"

// 動的プロパティ追加
Integer.metaClass.getSquare = {
    delegate * delegate
}

println "5の2乗: ${5.square}"

// MethodMissing(存在しないメソッドの処理)
class DynamicClass {
    def methodMissing(String name, args) {
        "呼び出されたメソッド: $name, 引数: $args"
    }
    
    def propertyMissing(String name) {
        "存在しないプロパティ: $name"
    }
}

def dynamic = new DynamicClass()
println dynamic.someMethod("arg1", "arg2")
println dynamic.someProperty

// カテゴリ(一時的なメタクラス変更)
class StringCategory {
    static String rot13(String self) {
        self.tr('A-Za-z', 'N-ZA-Mn-za-m')
    }
}

use(StringCategory) {
    println "'Hello'.rot13(): ${'Hello'.rot13()}"
}

// Expando(動的オブジェクト)
def expando = new Expando()
expando.name = "動的オブジェクト"
expando.greet = { "こんにちは、${name}です!" }
expando.age = 25

println expando.greet()

// AST変換(@Delegate例)
class CarEngine {
    def start() { "エンジン始動" }
    def stop() { "エンジン停止" }
}

class Car {
    @Delegate CarEngine engine = new CarEngine()
    String model
    
    Car(String model) {
        this.model = model
    }
}

def car = new Car("プリウス")
println car.start()  // CarEngineのメソッドが委譲される

DSL(ドメイン固有言語)構築

// ビルダーパターン
class HtmlBuilder {
    def level = 0
    
    def methodMissing(String name, args) {
        def indent = "  " * level
        if (args.length == 1 && args[0] instanceof Closure) {
            println "${indent}<$name>"
            level++
            args[0]()
            level--
            println "${indent}</$name>"
        } else {
            println "${indent}<$name>${args[0]}</$name>"
        }
    }
}

def html = new HtmlBuilder()

html.html {
    head {
        title "Groovy DSL Example"
    }
    body {
        h1 "Welcome to Groovy"
        p "This is a DSL example"
        div {
            span "Nested content"
        }
    }
}

// カスタムDSL例
class DatabaseDSL {
    def queries = []
    
    def select(String... columns) {
        queries << "SELECT ${columns.join(', ')}"
        this
    }
    
    def from(String table) {
        queries << "FROM $table"
        this
    }
    
    def where(String condition) {
        queries << "WHERE $condition"
        this
    }
    
    def build() {
        queries.join(' ')
    }
}

def query = new DatabaseDSL()
    .select("name", "age")
    .from("users")
    .where("age > 18")
    .build()

println "生成されたSQL: $query"

// 設定DSL
class ConfigDSL {
    def config = [:]
    
    def database(Closure closure) {
        def dbConfig = [:]
        closure.delegate = dbConfig
        closure.resolveStrategy = Closure.DELEGATE_FIRST
        closure()
        config.database = dbConfig
    }
    
    def server(Closure closure) {
        def serverConfig = [:]
        closure.delegate = serverConfig
        closure.resolveStrategy = Closure.DELEGATE_FIRST
        closure()
        config.server = serverConfig
    }
    
    def propertyMissing(String name, value) {
        this[name] = value
    }
}

def configDsl = new ConfigDSL()
configDsl.with {
    database {
        host = "localhost"
        port = 5432
        name = "myapp"
    }
    
    server {
        host = "0.0.0.0"
        port = 8080
        ssl = true
    }
}

println "設定: ${configDsl.config}"

Grape(依存関係管理)とスクリプト

// Grape依存関係の自動解決
@Grab('org.apache.commons:commons-csv:1.9.0')
import org.apache.commons.csv.*

// CSVデータの高度な処理
def csvData = """Name,Age,City
John,25,Tokyo
Jane,30,Osaka
Bob,35,Kyoto"""

def format = CSVFormat.DEFAULT.withFirstRecordAsHeader()
def parser = CSVParser.parse(csvData, format)

parser.forEach { record ->
    println "${record.get('Name')} (${record.get('Age')}) from ${record.get('City')}"
}

// RESTクライアント例
@Grab('org.apache.groovy:groovy-json:4.0.0')
import groovy.json.JsonSlurper

// 簡単なHTTPクライント
def url = "https://jsonplaceholder.typicode.com/posts/1"
def connection = new URL(url).openConnection()
def response = connection.inputStream.text
def json = new JsonSlurper().parseText(response)

println "タイトル: ${json.title}"
println "本文: ${json.body}"

// ワンライナースクリプト例
// groovy -e "println 'Hello from command line'"
// groovy -e "(1..10).each { println it * it }"
// groovy -e "new File('.').eachFile { println it.name }"

バージョン

バージョンステータス主要な特徴リリース年
Groovy 4.0LatestJava 17サポート、パフォーマンス改善2022
Groovy 3.0CurrentParrotパーサー、Java 14サポート2020
Groovy 2.5Maintainedマクロサポート、Android対応改善2018
Groovy 2.4Legacyトレイト導入、アノテーション改善2014

参考ページ

公式ドキュメント

学習リソース

フレームワークとツール

  • Gradle - ビルド自動化ツール
  • Grails - Webアプリケーションフレームワーク
  • Spock Framework - テストフレームワーク