Kotlinでシーケンスを活用して重複データを簡単に排除する方法

Kotlinで効率的にデータを処理するためのツールとして「シーケンス」は非常に有用です。特に、大量のデータセットを扱う際、重複を排除する操作は頻繁に必要とされます。Kotlinはdistinct関数を使用することで、この操作を簡単かつ直感的に行うことができます。本記事では、シーケンスとdistinctを組み合わせた使い方を詳しく解説し、実用的なコード例や応用シナリオを通じて、効率的なデータ操作のノウハウをお届けします。

目次
  1. Kotlinのシーケンスとは
    1. シーケンスの基本的な特徴
    2. シーケンスの作成方法
    3. シーケンスの利点
  2. シーケンスを使うメリット
    1. 1. メモリ使用量の削減
    2. 2. パフォーマンスの向上
    3. 3. 簡潔で読みやすいコード
    4. 4. 大規模データにおけるスケーラビリティ
    5. 5. 遅延評価の柔軟性
  3. distinct関数の使い方
    1. distinct関数の基本的な使用方法
    2. distinctの仕組み
    3. 文字列の重複排除
    4. distinctを使用した複雑なデータ型の処理
    5. toList()やtoSet()との違い
  4. 条件付きで重複を除外する方法
    1. distinctByの基本的な使用方法
    2. カスタムオブジェクトでの使用例
    3. 条件付きの一意性の具体例
    4. distinctByと遅延評価の組み合わせ
    5. 適切なシナリオでの使用
  5. 大量データ処理におけるパフォーマンスの向上
    1. 遅延評価の仕組み
    2. 中間リストの生成を防ぐ
    3. 無限シーケンスの利用
    4. シーケンスの性能比較
    5. 適用シナリオ
  6. 実例:重複するユーザー名の排除
    1. シナリオの説明
    2. 基本的なコード例
    3. 特定の条件を考慮した重複排除
    4. 大量データでの効率的な処理
    5. 実用例
  7. 注意点とベストプラクティス
    1. 注意点
    2. ベストプラクティス
    3. 結論
  8. 演習問題:独自のデータセットで試してみよう
    1. 演習1: 数字データの重複排除
    2. 演習2: カスタムオブジェクトの重複排除
    3. 演習3: 条件付きで最新データを取得
    4. 演習4: 無限シーケンスから一意なデータを抽出
    5. 解答の確認方法
  9. まとめ

Kotlinのシーケンスとは


Kotlinのシーケンスは、コレクションを効率的に処理するための遅延評価型のデータ構造です。通常のリストや配列とは異なり、シーケンスは要素を一つずつ順番に処理するため、大量のデータを扱う際にメモリ使用量を抑えることができます。

シーケンスの基本的な特徴

  • 遅延評価:操作が必要になるまで実行を遅らせるため、パフォーマンスを向上させます。
  • ストリームのような動作:操作がチェーン化され、データフローが簡潔になります。
  • コレクションとの相互変換:リストやセットから簡単にシーケンスを生成できます。

シーケンスの作成方法


シーケンスを作成するには、sequenceOfasSequenceを使用します。以下に基本的な例を示します:

// sequenceOfを使ったシーケンスの生成
val sequence = sequenceOf(1, 2, 3, 4, 5)

// リストをシーケンスに変換
val list = listOf(1, 2, 3, 4, 5)
val sequenceFromList = list.asSequence()

シーケンスの利点

  • 大規模データを効率的に処理できる。
  • 不要な中間リストを生成せず、メモリ負荷を削減。
  • フィルタリングやマッピング操作を簡潔に記述可能。

Kotlinのシーケンスは、パフォーマンスを重視したプログラムを作成する上で重要なツールです。本記事では、これを活用してデータの重複を効果的に排除する方法を見ていきます。

シーケンスを使うメリット

Kotlinのシーケンスは、大規模なデータ処理や効率性を求められるシナリオで非常に有用です。リストや配列などのコレクションに比べて、以下のような利点があります。

1. メモリ使用量の削減


シーケンスは遅延評価を利用するため、必要な要素だけをメモリにロードします。これにより、以下のような場面で効率的な処理が可能です。

  • 非常に大きなデータセットを扱う場合
  • フィルタリングや変換で多数の中間リストが発生する場合
val largeList = (1..1_000_000).toList()
val filteredSequence = largeList.asSequence().filter { it % 2 == 0 }
println(filteredSequence.take(10).toList()) // 必要な要素だけ処理

2. パフォーマンスの向上


中間操作(例:mapfilter)をまとめて実行し、最終操作(例:toListforEach)が呼び出されるまで処理を遅らせます。この遅延評価により、無駄な計算が省略されます。

3. 簡潔で読みやすいコード


シーケンスを使用することで、複雑なデータ操作を直感的に記述できます。以下は、フィルタリングと変換を組み合わせた例です:

val names = listOf("Alice", "Bob", "Charlie")
val filteredNames = names.asSequence()
    .filter { it.startsWith("A") }
    .map { it.uppercase() }
    .toList()
println(filteredNames) // [ALICE]

4. 大規模データにおけるスケーラビリティ


通常のコレクション操作では、全データを一度にメモリに読み込む必要があります。一方、シーケンスは逐次処理を行うため、スケールするデータ量に対しても安定したパフォーマンスを発揮します。

5. 遅延評価の柔軟性


途中で処理を停止する必要がある場合でも、シーケンスはその特性を活かして余計な計算を回避します。

val numbers = generateSequence(1) { it + 1 }
println(numbers.take(5).toList()) // [1, 2, 3, 4, 5]

シーケンスのこれらの特性により、大量データ処理や複雑な操作を効率よく実現できます。本記事では、これを基にdistinctを活用した重複排除の方法を解説していきます。

distinct関数の使い方

Kotlinでは、シーケンスにdistinct関数を使用することで、簡単に重複を排除することができます。この関数は、データセット内の一意の要素だけを残すようにフィルタリングします。

distinct関数の基本的な使用方法


以下は、distinctを使用して重複を除外する基本的な例です:

val numbers = sequenceOf(1, 2, 2, 3, 4, 4, 5)
val distinctNumbers = numbers.distinct().toList()
println(distinctNumbers) // [1, 2, 3, 4, 5]

このコードでは、distinctが重複する値を自動的に除外し、一意の値のみを返します。

distinctの仕組み


distinctは、内部でハッシュセットを使用して既に処理した要素を記録します。そのため、以下の特徴を持ちます:

  • 処理順序は入力順序に従います。
  • 大規模データセットでも効率的に動作しますが、メモリ消費はデータ量に比例します。

文字列の重複排除


文字列データでも同様に利用できます:

val names = sequenceOf("Alice", "Bob", "Alice", "Charlie", "Bob")
val uniqueNames = names.distinct().toList()
println(uniqueNames) // [Alice, Bob, Charlie]

distinctを使用した複雑なデータ型の処理


カスタムオブジェクトを扱う場合は、デフォルトの動作が異なる場合があります。例えば、以下のようなデータクラスを持つリストを処理するとします:

data class Person(val name: String, val age: Int)

val people = sequenceOf(
    Person("Alice", 25),
    Person("Bob", 30),
    Person("Alice", 25),
    Person("Charlie", 35)
)

val distinctPeople = people.distinct().toList()
println(distinctPeople) // [Person(name=Alice, age=25), Person(name=Bob, age=30), Person(name=Charlie, age=35)]

ここでは、PersonクラスがequalshashCodeを適切に実装しているため、重複が正しく除外されます。

toList()やtoSet()との違い


toList()toSet()を使って重複を排除することも可能ですが、以下のような違いがあります:

  • toSet: 順序が保証されない。
  • distinct: 順序が保証される。

シーケンスのdistinctを利用することで、大規模データ処理において順序を保持しながら重複を排除する柔軟な操作が可能です。次のセクションでは、条件付きで重複を除外する方法について解説します。

条件付きで重複を除外する方法

distinct関数は基本的に要素全体の重複を除外しますが、特定の条件に基づいて重複を排除したい場合には、distinctBy関数が非常に便利です。この関数では、要素の一部や特定のプロパティに基づいて一意性を判定できます。

distinctByの基本的な使用方法


distinctByを使用することで、特定の条件で重複を除外できます。以下は簡単な例です:

val numbers = sequenceOf(10, 20, 30, 21, 31, 10)
val distinctByTens = numbers.distinctBy { it / 10 }.toList()
println(distinctByTens) // [10, 21]

この例では、10で割った値(商)を基準にして重複を除外しています。

カスタムオブジェクトでの使用例


複雑なデータ型でも、プロパティを指定してdistinctByを活用できます。以下の例では、人の名前を基準に重複を排除します:

data class Person(val name: String, val age: Int)

val people = sequenceOf(
    Person("Alice", 25),
    Person("Bob", 30),
    Person("Alice", 28),
    Person("Charlie", 35)
)

val distinctByName = people.distinctBy { it.name }.toList()
println(distinctByName) // [Person(name=Alice, age=25), Person(name=Bob, age=30), Person(name=Charlie, age=35)]

このコードでは、名前が重複する要素の中で、最初に現れたものだけが残ります。

条件付きの一意性の具体例


条件付きのdistinctByを使うことで、データをさらに柔軟に操作できます。以下は、最も若い年齢の人を名前ごとに一意にする例です:

val youngestByName = people
    .groupBy { it.name }
    .map { (_, group) -> group.minByOrNull { it.age }!! }
    .asSequence()

println(youngestByName.toList()) 
// [Person(name=Alice, age=25), Person(name=Bob, age=30), Person(name=Charlie, age=35)]

この例では、groupByを活用して同じ名前を持つ人をグループ化し、その中で最も若い人を選んでいます。

distinctByと遅延評価の組み合わせ


シーケンスの遅延評価特性を活かして、必要な処理だけを実行し、無駄を最小限に抑えることができます:

val largeDataset = generateSequence(1) { it + 1 }
    .map { it * 2 }
    .distinctBy { it % 10 }
    .take(5)
    .toList()

println(largeDataset) // [2, 4, 6, 8, 10]

この例では、生成された無限シーケンスから、it % 10の値が一意になるように重複を排除し、最初の5つの要素を取得しています。

適切なシナリオでの使用

  • 重複排除の基準が単純でない場合
  • 大規模データの効率的な処理が求められる場合
  • オブジェクトのプロパティに基づいて一意性を判断したい場合

条件付き重複排除の活用により、データ操作の柔軟性が大幅に向上します。次のセクションでは、大量データ処理におけるシーケンスのパフォーマンス向上について詳しく見ていきます。

大量データ処理におけるパフォーマンスの向上

Kotlinのシーケンスは、大量データを効率的に処理する際に大きなメリットを発揮します。その主要な要因は「遅延評価」の仕組みにあります。このセクションでは、シーケンスの特性がどのようにパフォーマンスを向上させるかを詳しく解説します。

遅延評価の仕組み


遅延評価とは、必要なタイミングになるまでデータ処理を遅らせる仕組みのことです。以下のコードでその仕組みを確認できます:

val numbers = (1..1_000_000).asSequence()
    .filter { it % 2 == 0 }
    .map { it * 2 }
    .take(5)
    .toList()

println(numbers) // [4, 8, 12, 16, 20]

この例では、以下のような手順で処理が進みます:

  1. 数字を1から1,000,000まで生成。
  2. 偶数のみフィルタリング。
  3. フィルタリングされた数値を2倍に変換。
  4. 最初の5つの要素を取得。

ポイントは、フィルタリングと変換の処理が「必要な5つの要素分だけ」実行されることです。全体を一度に処理する通常のコレクションとは異なり、シーケンスは必要な部分だけを計算します。

中間リストの生成を防ぐ


通常のリスト操作では、各操作(フィルタリングやマッピング)ごとに中間リストが生成されます。一方、シーケンスでは中間リストを生成せずに、要素を順番に処理するため、メモリ使用量が大幅に削減されます。

// リスト操作:中間リストが生成される
val listResult = (1..1_000_000)
    .filter { it % 2 == 0 }
    .map { it * 2 }
    .take(5)
println(listResult) // [4, 8, 12, 16, 20]

このコードでは、filtermapのたびに中間リストが生成され、メモリ消費が増加します。

無限シーケンスの利用


シーケンスは、データの量が事前に決まっていない場合でも効率的に処理を行えます。以下は無限シーケンスを用いた例です:

val infiniteSequence = generateSequence(1) { it + 1 }
    .filter { it % 3 == 0 }
    .map { it * 2 }
    .take(10)
    .toList()

println(infiniteSequence) // [6, 12, 18, 24, 30, 36, 42, 48, 54, 60]

この例では、必要なデータが取得されるまでシーケンスが処理を続けます。これにより、大量データや無限データを効率的に扱うことが可能になります。

シーケンスの性能比較


以下は、リストとシーケンスを使用した処理速度の比較です:

// リストの場合
val listStart = System.currentTimeMillis()
val listResult = (1..1_000_000)
    .filter { it % 2 == 0 }
    .map { it * 2 }
    .toList()
val listEnd = System.currentTimeMillis()
println("List processing time: ${listEnd - listStart} ms")

// シーケンスの場合
val sequenceStart = System.currentTimeMillis()
val sequenceResult = (1..1_000_000).asSequence()
    .filter { it % 2 == 0 }
    .map { it * 2 }
    .toList()
val sequenceEnd = System.currentTimeMillis()
println("Sequence processing time: ${sequenceEnd - sequenceStart} ms")

結果として、シーケンスのほうが中間リストを生成しない分、メモリ使用量と処理時間が少なくなることが確認できます。

適用シナリオ

  • 大規模データセットの処理。
  • 無限データストリームの操作。
  • 中間リストの生成を回避したい場合。

シーケンスを活用すれば、大量データ処理の効率性が飛躍的に向上します。次のセクションでは、具体的なユースケースとして、重複するユーザー名を排除する実例を紹介します。

実例:重複するユーザー名の排除

Kotlinのシーケンスとdistinctを使用することで、重複するユーザー名を簡単に排除できます。このセクションでは、実際のコード例を通じて、どのようにデータセットを操作できるかを解説します。

シナリオの説明


以下のようなユーザー情報のリストを考えます:

  • データには名前とメールアドレスが含まれている。
  • 名前が重複するエントリがある。
  • ユニークな名前を持つユーザーだけをリスト化したい。

基本的なコード例


以下のコードでは、distinctByを利用して、名前の重複を排除します:

data class User(val name: String, val email: String)

fun main() {
    val users = sequenceOf(
        User("Alice", "alice@example.com"),
        User("Bob", "bob@example.com"),
        User("Alice", "alice.duplicate@example.com"),
        User("Charlie", "charlie@example.com"),
        User("Bob", "bob.duplicate@example.com")
    )

    val uniqueUsers = users.distinctBy { it.name }.toList()

    println(uniqueUsers)
}

実行結果:

[User(name=Alice, email=alice@example.com), User(name=Bob, email=bob@example.com), User(name=Charlie, email=charlie@example.com)]

このコードでは、名前を基準に一意性を判断し、最初に現れたユーザーのみを残しています。

特定の条件を考慮した重複排除


ユーザー名が同じ場合、最新のメールアドレスを保持したい場合は、以下のようにグループ化して条件を適用できます:

fun main() {
    val users = sequenceOf(
        User("Alice", "alice@example.com"),
        User("Bob", "bob@example.com"),
        User("Alice", "alice.duplicate@example.com"),
        User("Charlie", "charlie@example.com"),
        User("Bob", "bob.duplicate@example.com")
    )

    val uniqueUsers = users
        .groupBy { it.name }
        .map { (_, group) -> group.last() }
        .asSequence()
        .toList()

    println(uniqueUsers)
}

実行結果:

[User(name=Alice, email=alice.duplicate@example.com), User(name=Bob, email=bob.duplicate@example.com), User(name=Charlie, email=charlie@example.com)]

このコードでは、groupByを使用して名前ごとにグループ化し、最後に現れたデータを選択しています。

大量データでの効率的な処理


シーケンスを使用することで、大規模データセットのメモリ消費を最小限に抑えつつ、同様の操作が可能です:

val largeDataset = generateSequence { User("User${(1..100).random()}", "email@example.com") }
    .take(1_000_000)
    .distinctBy { it.name }
    .toList()

println("Unique users count: ${largeDataset.size}")

この例では、無作為に生成された1,000,000件のユーザー情報から、名前の重複を排除し、一意なユーザー数を取得しています。

実用例

  • 顧客リストの重複削除。
  • ユーザー名やメールアドレスの一意性チェック。
  • 名前やIDに基づいたデータの正規化。

シーケンスとdistinctを組み合わせることで、柔軟かつ効率的なデータ操作が可能になります。次のセクションでは、これらの操作を行う際の注意点とベストプラクティスについて解説します。

注意点とベストプラクティス

KotlinのシーケンスとdistinctdistinctByを使用する際には、いくつかの注意点と最適な使い方を理解しておくことが重要です。このセクションでは、これらのポイントを解説します。

注意点

1. メモリ消費と遅延評価


シーケンスは遅延評価によりメモリ効率が良いですが、distinctdistinctByは内部でハッシュセットを使用して重複を追跡します。そのため、以下のような場合に注意が必要です:

  • 非常に大きなデータセットでdistinctを使用する場合、ハッシュセットのメモリ消費が大きくなる可能性があります。
  • 無限シーケンスにdistinctを使用する際は、メモリが足りなくなることがあります。

対策: 必要に応じてデータを小さなチャンクに分割するか、遅延評価の範囲を限定します。

2. オブジェクトの`equals`と`hashCode`


distinctequalshashCodeメソッドを使用して重複を判断します。そのため、カスタムオブジェクトで正しく動作させるには、これらのメソッドが適切に実装されている必要があります。

data class Person(val name: String, val age: Int) {
    override fun equals(other: Any?) = other is Person && name == other.name
    override fun hashCode() = name.hashCode()
}

注意: データクラスはデフォルトでequalshashCodeを提供しますが、必要に応じてカスタマイズすることを検討してください。

3. 順序保証


distinctは順序を保持しますが、内部の処理で順序が変わる可能性のあるメソッド(例:groupBy)を併用する場合は注意が必要です。
対策: 必要な場合は元の順序を明示的に保存する操作を追加します。

4. パフォーマンスのトレードオフ


遅延評価が役立つケースではパフォーマンスが向上しますが、distinctの使用にはハッシュ計算が伴います。そのため、小規模なデータではリスト操作がシンプルで高速な場合もあります。
対策: データ規模や操作内容に応じて、シーケンスかリストを使い分けます。

ベストプラクティス

1. 無限シーケンスでの操作


無限シーケンスにdistinctdistinctByを適用する場合、必ず最終操作(例:take)を使用して処理を制限します。

val uniqueNumbers = generateSequence(1) { it + 1 }
    .distinctBy { it % 10 }
    .take(10)
    .toList()

println(uniqueNumbers) // [1, 2, 3, ..., 10]

2. 条件に基づいたフィルタリング


distinctByを使うことで、特定の条件に基づいた柔軟な重複排除を実現できます。これにより、中間リストを削減し、効率的なデータ操作が可能です。

3. 遅延評価の活用


シーケンスを活用する場合、可能な限り遅延評価を維持するよう設計します。複数の中間操作を組み合わせる際には、最終操作を明確に指定します。

4. コードの可読性を重視


複雑なデータ処理を行う場合でも、チェーン操作を適切に整理し、意図を明確に記述することで可読性を向上させます。コメントや関数分割も有効です。

5. デバッグとテストの実施

  • 大量データを扱う際は、distinctの挙動を小さなデータセットでテストしてから適用します。
  • ユニットテストでdistinctByが期待通りに動作するかを確認します。

結論


distinctdistinctByを適切に使用することで、シーケンスを活用した効率的なデータ処理が可能になります。次のセクションでは、演習問題を通じてさらに理解を深められる内容を提供します。

演習問題:独自のデータセットで試してみよう

これまでに学んだKotlinのシーケンスとdistinctの使い方を実践するために、いくつかの演習問題を用意しました。以下の課題を解いて、理解を深めてください。

演習1: 数字データの重複排除


以下の数字データセットから、10で割った余りが一意となる数字を取得してください。結果は昇順で表示してください。

val numbers = sequenceOf(15, 25, 35, 12, 22, 45, 10, 50, 60)

期待する結果:

[15, 12, 10]

ヒント:

  • distinctByを使用して余りを基準に一意性を判断します。
  • 結果を昇順にするにはsortedを使用します。

演習2: カスタムオブジェクトの重複排除


以下のProductデータクラスを使用して、カテゴリ(category)ごとに一意な商品リストを作成してください。結果には、カテゴリごとに最初に現れた商品だけを含めるようにしてください。

data class Product(val id: Int, val name: String, val category: String)

val products = sequenceOf(
    Product(1, "Laptop", "Electronics"),
    Product(2, "Phone", "Electronics"),
    Product(3, "Shirt", "Clothing"),
    Product(4, "Pants", "Clothing"),
    Product(5, "Mug", "Home"),
    Product(6, "Lamp", "Home")
)

期待する結果:

[Product(id=1, name=Laptop, category=Electronics), Product(id=3, name=Shirt, category=Clothing), Product(id=5, name=Mug, category=Home)]

ヒント:

  • distinctByを使用してcategoryを基準にします。

演習3: 条件付きで最新データを取得


以下のStudentデータクラスから、各学年(grade)の中で最も高い点数を取得した学生だけをリスト化してください。

data class Student(val name: String, val grade: Int, val score: Int)

val students = sequenceOf(
    Student("Alice", 10, 85),
    Student("Bob", 10, 92),
    Student("Charlie", 11, 88),
    Student("David", 11, 91),
    Student("Eve", 12, 89),
    Student("Frank", 12, 87)
)

期待する結果:

[Student(name=Bob, grade=10, score=92), Student(name=David, grade=11, score=91), Student(name=Eve, grade=12, score=89)]

ヒント:

  • groupByで学年ごとにグループ化します。
  • グループ内で最高点の学生を選ぶためにmaxByOrNullを使用します。

演習4: 無限シーケンスから一意なデータを抽出


無限シーケンスを生成し、10で割った余りが一意となる最初の10個の数字を取得してください。

val infiniteSequence = generateSequence(1) { it + 1 }

期待する結果:

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

ヒント:

  • 無限シーケンスにはdistinctByを適用し、takeで最初の10個を取得します。

解答の確認方法


各演習のコードをKotlinプロジェクトで実行し、出力結果が期待する結果と一致するかを確認してください。また、コードをアレンジしてさまざまな条件で試すことで、さらに理解を深められます。

次のセクションでは、本記事のまとめを行います。

まとめ

本記事では、Kotlinでシーケンスを活用してデータの重複を効率的に除外する方法について解説しました。distinctdistinctByを使うことで、シンプルな重複排除から条件付きのユニークデータ取得まで、幅広いデータ処理が可能になります。また、シーケンスの遅延評価特性を活かせば、大規模データや無限データの操作も効率的に行えます。

演習問題を通じて、基本的な使い方から応用的な処理方法までを体験できたはずです。これを機に、Kotlinでのデータ処理スキルをさらに高め、実践的なプログラムに役立ててください。

コメント

コメントする

目次
  1. Kotlinのシーケンスとは
    1. シーケンスの基本的な特徴
    2. シーケンスの作成方法
    3. シーケンスの利点
  2. シーケンスを使うメリット
    1. 1. メモリ使用量の削減
    2. 2. パフォーマンスの向上
    3. 3. 簡潔で読みやすいコード
    4. 4. 大規模データにおけるスケーラビリティ
    5. 5. 遅延評価の柔軟性
  3. distinct関数の使い方
    1. distinct関数の基本的な使用方法
    2. distinctの仕組み
    3. 文字列の重複排除
    4. distinctを使用した複雑なデータ型の処理
    5. toList()やtoSet()との違い
  4. 条件付きで重複を除外する方法
    1. distinctByの基本的な使用方法
    2. カスタムオブジェクトでの使用例
    3. 条件付きの一意性の具体例
    4. distinctByと遅延評価の組み合わせ
    5. 適切なシナリオでの使用
  5. 大量データ処理におけるパフォーマンスの向上
    1. 遅延評価の仕組み
    2. 中間リストの生成を防ぐ
    3. 無限シーケンスの利用
    4. シーケンスの性能比較
    5. 適用シナリオ
  6. 実例:重複するユーザー名の排除
    1. シナリオの説明
    2. 基本的なコード例
    3. 特定の条件を考慮した重複排除
    4. 大量データでの効率的な処理
    5. 実用例
  7. 注意点とベストプラクティス
    1. 注意点
    2. ベストプラクティス
    3. 結論
  8. 演習問題:独自のデータセットで試してみよう
    1. 演習1: 数字データの重複排除
    2. 演習2: カスタムオブジェクトの重複排除
    3. 演習3: 条件付きで最新データを取得
    4. 演習4: 無限シーケンスから一意なデータを抽出
    5. 解答の確認方法
  9. まとめ