JAVA/TESTSGO →TS →

Collections

TreeSet: Comparatorが0を返す値は同じ要素として扱う

TreeSet は equals ではなく Comparator の比較結果で、要素の重複と検索を判断します。

Java 8+
pitfallcollectionstreesetcomparator
01 / 01 proof
SourceComparatorEquivalence.java
Java 21
package examples.sortedset;

import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;

public final class ComparatorEquivalence {
  private ComparatorEquivalence() {}

  public static Set<String> uniqueByLength(String... values) {
    var words = new TreeSet<String>(Comparator.comparingInt(String::length));
    for (var value : values) {
      words.add(value);
    }
    return words;
  }
}
Source✓Test
TestComparatorEquivalenceTest.java
Java 21
package examples.sortedset;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import org.junit.jupiter.api.Test;

class ComparatorEquivalenceTest {
  @Test
  void comparatorが0を返す異なる値はTreeSetで同じ要素として扱われる() {
    var words = ComparatorEquivalence.uniqueByLength("cat", "dog", "java");

    assertEquals(List.of("cat", "java"), List.copyOf(words));
    assertEquals(2, words.size());
  }

  @Test
  void comparatorが0を返す値は登録していなくてもcontainsで見つかる() {
    var words = ComparatorEquivalence.uniqueByLength("cat");

    assertTrue(words.contains("dog"));
  }
}
01Assertion

このテストで確認できること

  • 文字列の長さだけを比較する Comparator では、同じ長さの別の文字列を重複として扱う
  • 登録していない同じ長さの文字列も contains で見つかる

TreeSet と TreeMap は compare が 0 を返すキーを同じものとして扱います。equals と異なる比較規則を使うと、集合の要素数や検索結果が直感とずれるため、Comparator の等価性を設計として明示します。

OBSERVED RESULTexpected: [cat, java] actual: [cat, java] expected: contains("dog") → true actual: true

Static index

教材を検索

テーマ、タグ、テスト名、カテゴリ、Javaバージョンで絞り込みます。

55件の教材を表示しています。

Language · Java 8+String: == と equals@Test 同じ内容のStringはequalsで等しいLanguage · Java 11+String: isBlank は空白だけの文字列も判定する@Test 空白だけのStringはisBlankでtrueになるLanguage · Java 8+StringBuilder: append は同じインスタンスを変更する@Test StringBuilderのappendは同じインスタンスを返すCollections · Java 8+ArrayList: add は元の List を変更する@Test addすると元のListに要素が追加されるCollections · Java 9+List.of: 変更できない List を作る@Test ListOfが返すListには要素を追加できないCollections · Java 8+Arrays.asList: 配列を共有する固定長 List@Test ArraysAsListで変更した要素は元の配列にも反映されるCollections · Java 8+Map: computeIfAbsent で既定値を登録する@Test 存在しないキーには既定値が登録されるNumbers · Java 8+Integer: == はキャッシュ範囲でだけ同じ参照になり得る@Test Integerの128は参照比較では等しくないNumbers · Java 8+BigDecimal: equals と compareTo は scale の扱いが違う@Test scaleが異なっても数値として同じBigDecimalはcompareToで等しいLanguage · Java 16+record: 同じ構成要素なら equals で等しい@Test 同じ値を持つrecordはequalsで等しくなるLanguage · Java 8+equals と hashCode: HashSetで値を見つけるための契約@Test equalsとhashCodeを実装した値はHashSetで検索できるFunctional · Java 8+Optional: orElse で既定値を返す@Test emptyのOptionalは既定値を返すFunctional · Java 8+Optional: orElse と orElseGet は既定値の評価タイミングが違う@Test orElseはOptionalに値があっても引数を評価するFunctional · Java 8+Optional: map と flatMap はOptionalのネストをどう扱うかが違う@Test flatMapはOptionalを返す関数の結果を平坦化するFunctional · Java 8+Stream: filter で条件に合う値を残す@Test filterは正の数だけを残すFunctional · Java 8+Stream: 中間操作は終端操作まで評価されない@Test Streamは終端操作までmapを評価しないFunctional · Java 16+Stream.toList: 変更できない List を返す@Test StreamToListが返すListには要素を追加できないError Handling · Java 8+Exception: assertThrows で例外の契約を確かめる@Test ゼロで割るとArithmeticExceptionが送出されるError Handling · Java 8+Exception: checked と unchecked は呼び出し側の契約が違う@Test 不正な引数はuncheckedのIllegalArgumentExceptionを送出するGenerics · Java 8+generics: ? extends Number で数値型を受け取る@Test IntegerのListをNumberとして合計できるGenerics · Java 8+generics: ? extends は読む側、? super は書く側の境界@Test superはNumberのListへIntegerを追加できるDate / Time · Java 8+Date / Time: LocalDate と LocalDateTime は時刻情報の有無が違う@Test LocalDateから作るLocalDateTimeはその日の開始時刻になるError Handling · Java 7+try-with-resources: 複数resourceは逆順にcloseされる@Test tryWithResourcesは処理後にcloseを必ず呼ぶCollections · Java 8+HashMap: 可変のキーを変更すると登録済みの値を検索できない@Test HashMapへ登録した後にキーを変更すると同じキーで検索できないNumbers · Java 8+BigDecimal: 除算の丸めモードで同じ式の結果が変わる@Test BigDecimalの除算は指定した丸めモードで結果が変わるFunctional · Java 8+Optional: ofNullable はnullをemptyにし、ofは例外にする@Test ofNullableはnullをemptyのOptionalへ変換するCollections · Java 8+HashMap: 衝突は処理できるがequalsとhashCodeの不整合は検索を壊す@Test equalsがtrueでもhashCodeが違うキーでは登録済みの値を検索できないNumbers · Java 8+BigDecimal: doubleから作ると意図しない桁が入る@Test doubleから作るBigDecimalは10進数の0point1と厳密には一致しないFunctional · Java 8+Optional: フィールドや引数にするとnullとemptyが二重になる@Test Optional型の引数にもnullを渡せるため不在表現が二重になるLanguage · Java 8+String: length はUTF-16のコード単位を数える@Test 絵文字はUTF16のlengthでは2だがcodePointCountでは1になるLanguage · Java 8+String: 見た目が同じUnicode文字列でもequalsが異なる@Test 見た目が同じでも合成済みと結合文字の文字列はequalsで異なるCollections · Java 8+HashMap: get のnullだけでは未登録とnull値を区別できない@Test getだけではnull値のキーと未登録キーを区別できないCollections · Java 9+List.of: null要素を許可しない@Test ListOfはnull要素を持つListを作れないFunctional · Java 8+Stream: 終端操作の後に同じStreamは使えない@Test 終端操作を実行したStreamは再利用できないDate / Time · Java 8+ZonedDateTime: plusDays と plusHours は夏時間で異なる@Test 夏時間開始日のplusDaysとplusHoursは同じ時刻にならないLanguage · Java 16+record: 配列の構成要素は値ではなく参照で比較される@Test 同じ要素の別配列を持つrecordはequalsで等しくならないLanguage · Java 21+sealed class: 許可した型だけを網羅的にswitchできる@Test sealed型の全許可サブタイプをswitchで分岐できるConcurrency · Java 21+virtual thread: 通常のThreadLocalは親から引き継がれない@Test virtualThreadには親スレッドの通常のThreadLocal値が引き継がれないConcurrency · Java 8+CompletableFuture: join は失敗をCompletionExceptionで包む@Test joinは非同期処理の失敗をCompletionExceptionで包むConcurrency · Java 8+ConcurrentHashMap: nullキーとnull値を許可しない@Test ConcurrentHashMapはnullキーとnull値を許可しないGenerics · Java 8+generics: 型引数は実行時に消去される@Test 異なる型引数のArrayListは実行時には同じクラスであるCollections · Java 8+TreeSet: Comparatorが0を返す値は同じ要素として扱う@Test Comparatorが0を返す異なる値はTreeSetで同じ要素として扱われるCollections · Java 10+defensive copy: 入力Listの後続変更から状態を守る@Test constructorは入力Listをコピーして後からの変更を受けないCollections · Java 10+Collections: unmodifiable view と immutable copy は共有状態が違う@Test unmodifiableViewは元のListの後続変更を反映するFunctional · Java 8+Collectors.toMap: 重複キーにはmerge規則が必要@Test toMapは同じキーがあるとmerge関数なしでは例外にするLanguage · Java 8+enum: 定数は同じインスタンスとして比較できる@Test valueOfで得たenum定数は宣言済み定数と同じインスタンスであるLanguage · Java 8+annotation: RUNTIME保持ならreflectionから読める@Test RUNTIME保持のannotationはreflectionから取得できるLanguage · Java 8+class initialization: static fieldは宣言順に初期化される@Test staticFieldは宣言順に初期化されるLanguage · Java 8+overload: wideningはboxingより先に選ばれる@Test primitiveのshortはboxingよりwideningを優先してlongを選ぶLanguage · Java 8+varargs: 配列を渡すと同じ配列を受け取る@Test 配列を渡すvarargsは配列をコピーせず同じ配列を受け取るNumbers · Java 8+unboxing: nullのIntegerを計算するとNullPointerExceptionになる@Test nullのIntegerを計算するとunboxing時にNullPointerExceptionになるLanguage · Java 8+String pool: リテラルとnew Stringは参照共有が違う@Test 同じリテラルはpoolの同じ参照を共有するConcurrency · Java 8+ExecutorService: submitした例外はFutureで観測する@Test submitしたタスクの例外はFutureのgetでExecutionExceptionとして観測するConcurrency · Java 8+interruption: Thread.interruptedは割り込みフラグをclearする@Test ThreadInterruptedは現在の割り込み状態を返してclearするConcurrency · Java 8+synchronized: 同じスレッドは同じmonitorへ再入できる@Test 同じスレッドは保持中のsynchronizedモニタを再入できる