1. 概要

MapがJavaでキーと値のペアを保持していることはわかっています。 テキストファイルのコンテンツをロードして、Java Mapに変換したい場合があります。

このクイックチュートリアルでは、それを実現する方法を探りましょう。

2. 問題の紹介

Map はキー値エントリを格納するため、ファイルのコンテンツをJava Map オブジェクトにインポートする場合、ファイルは特定の形式に従う必要があります。

サンプルファイルはそれを素早く説明するかもしれません:

$ cat theLordOfRings.txt
title:The Lord of the Rings: The Return of the King
director:Peter Jackson
actor:Sean Astin
actor:Ian McKellen
Gandalf and Aragorn lead the World of Men against Sauron's
army to draw his gaze from Frodo and Sam as they approach Mount Doom with the One Ring.

theLordOfRings.txt ファイルでわかるように、コロン文字を区切り文字と見なすと、ほとんどの行は「 KEY:VALUE」のパターンに従います。監督:ピーター・ジャクソン」。

したがって、各行を読み取り、キーと値を解析して、Mapオブジェクトに配置できます。

ただし、注意が必要な特殊なケースがいくつかあります。

  • 区切り文字を含む値–値は切り捨てられません。 たとえば、最初の行「 title:ロードオブザリング:リターン…
  • 重複したキー– 3つの戦略:既存のキーを上書きする、後者を破棄する、要件に応じて値をリストに集約する。 たとえば、ファイルには2つの「actor」キーがあります。
  • KEY:VALUE 」パターンに従わない行–行はスキップする必要があります。 たとえば、ファイルの最後の2行を参照してください。

次に、このファイルを読み取って、Java Mapオブジェクトに保存しましょう。

3. DupKeyOption列挙型

すでに説明したように、重複キーの場合には、上書き、破棄、および集約の3つのオプションがあります。

さらに、上書きまたは破棄オプションを使用すると、 地図タイプの地図 。 ただし、重複するキーの値を集計する場合は、次のような結果が得られます。 地図 >>

それでは、最初に上書きと破棄のシナリオを調べてみましょう。 最後に、スタンドアロンセクションで集計オプションについて説明します。

ソリューションを柔軟にするために、 enum クラスを作成して、オプションをパラメーターとしてソリューションメソッドに渡すことができるようにします。

enum DupKeyOption {
    OVERWRITE, DISCARD
}

4. BufferedReaderおよびFileReaderクラスの使用

BufferedReaderとFileReaderを組み合わせて、ファイルから1行ずつコンテンツを読み取ることができます

4.1. byBufferedReaderメソッドの作成

BufferedReaderFileReaderに基づいてメソッドを作成しましょう。

public static Map<String, String> byBufferedReader(String filePath, DupKeyOption dupKeyOption) {
    HashMap<String, String> map = new HashMap<>();
    String line;
    try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
        while ((line = reader.readLine()) != null) {
            String[] keyValuePair = line.split(":", 2);
            if (keyValuePair.length > 1) {
                String key = keyValuePair[0];
                String value = keyValuePair[1];
                if (DupKeyOption.OVERWRITE == dupKeyOption) {
                    map.put(key, value);
                } else if (DupKeyOption.DISCARD == dupKeyOption) {
                    map.putIfAbsent(key, value);
                }
            } else {
                System.out.println("No Key:Value found in line, ignoring: " + line);
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return map;
}

byBufferedReader メソッドは、入力ファイルパスと、重複したキーを持つエントリの処理方法を決定するdupKeyOptionオブジェクトの2つのパラメーターを受け入れます。

上記のコードが示すように、指定された入力ファイルから行を読み取るためにBufferedReaderオブジェクトを定義しました。 次に、whileループの各行を解析して処理します。 ウォークスルーして、それがどのように機能するかを理解しましょう。

  • BufferedReader オブジェクトを作成し、 try-with-resourcesを使用して、リーダーオブジェクトが自動的に閉じられるようにします
  • split メソッドをlimitパラメーターとともに使用して、コロン文字が含まれている場合に値の部分をそのまま保持します
  • 次に、 if チェックにより、「 KEY:VALUE」パターンと一致しない行が除外されます。
  • キーが重複している場合、「上書き」戦略を採用する場合は、 map.put(key、value)を呼び出すだけです。
  • それ以外の場合、 putIfAbsentメソッドを呼び出すと、重複したキーを持つ後者のエントリを無視できます

次に、メソッドが期待どおりに機能するかどうかをテストしましょう。

4.2. ソリューションのテスト

対応するテストメソッドを作成する前に、予想されるエントリを含む2つのマップオブジェクトを初期化します。

private static final Map<String, String> EXPECTED_MAP_DISCARD = Stream.of(new String[][]{
    {"title", "The Lord of the Rings: The Return of the King"},
    {"director", "Peter Jackson"},
    {"actor", "Sean Astin"}
  }).collect(Collectors.toMap(data -> data[0], data -> data[1]));

private static final Map<String, String> EXPECTED_MAP_OVERWRITE = Stream.of(new String[][]{
...
    {"actor", "Ian McKellen"}
  }).collect(Collectors.toMap(data -> data[0], data -> data[1]));

ご覧のとおり、テストアサーションに役立つ2つのMapオブジェクトを初期化しました。 1つは重複キーを破棄する場合で、もう1つはキーを上書きする場合です。

次に、メソッドをテストして、期待されるMapオブジェクトを取得できるかどうかを確認しましょう。

@Test
public void givenInputFile_whenInvokeByBufferedReader_shouldGetExpectedMap() {
    Map<String, String> mapOverwrite = FileToHashMap.byBufferedReader(filePath, FileToHashMap.DupKeyOption.OVERWRITE);
    assertThat(mapOverwrite).isEqualTo(EXPECTED_MAP_OVERWRITE);

    Map<String, String> mapDiscard = FileToHashMap.byBufferedReader(filePath, FileToHashMap.DupKeyOption.DISCARD);
    assertThat(mapDiscard).isEqualTo(EXPECTED_MAP_DISCARD);
}

実行すると、テストに合格します。 だから、私たちは問題を解決しました。

5. Java Streamを使用する

StreamはJava8から存在しています。 また、 Files.linesメソッドは、ファイル内のすべての行を含むStreamオブジェクトを便利に返すことができます。

それでは、 Stream を使用して蛾を作成し、問題を解決しましょう。

public static Map<String, String> byStream(String filePath, DupKeyOption dupKeyOption) {
    Map<String, String> map = new HashMap<>();
    try (Stream<String> lines = Files.lines(Paths.get(filePath))) {
        lines.filter(line -> line.contains(":"))
            .forEach(line -> {
                String[] keyValuePair = line.split(":", 2);
                String key = keyValuePair[0];
                String value = keyValuePair[1];
                if (DupKeyOption.OVERWRITE == dupKeyOption) {
                    map.put(key, value);
                } else if (DupKeyOption.DISCARD == dupKeyOption) {
                    map.putIfAbsent(key, value);
                }
            });
    } catch (IOException e) {
        e.printStackTrace();
    }
    return map;
}

上記のコードが示すように、メインロジックはbyBufferedReaderメソッドと非常によく似ています。 すばやく通過しましょう:

  • Stream オブジェクトには開いているファイルへの参照が含まれているため、Streamオブジェクトでtry-with-resourcesを引き続き使用しています。 ストリームを閉じてファイルを閉じる必要があります。
  • filter メソッドは、「 KEY:VALUE」パターンに従わないすべての行をスキップします。
  • forEach メソッドは、byBufferedReaderソリューションのwhileブロックとほとんど同じです。

最後に、byStreamソリューションをテストしてみましょう。

@Test
public void givenInputFile_whenInvokeByStream_shouldGetExpectedMap() {
    Map<String, String> mapOverwrite = FileToHashMap.byStream(filePath, FileToHashMap.DupKeyOption.OVERWRITE);
    assertThat(mapOverwrite).isEqualTo(EXPECTED_MAP_OVERWRITE);

    Map<String, String> mapDiscard = FileToHashMap.byStream(filePath, FileToHashMap.DupKeyOption.DISCARD);
    assertThat(mapDiscard).isEqualTo(EXPECTED_MAP_DISCARD);
}

テストを実行すると、テストにも合格します。

6. キーによる値の集計

これまで、上書きと破棄のシナリオに対する解決策を見てきました。 ただし、これまでに説明したように、必要に応じて、キーごとに値を集計することもできます。 したがって、最終的には、 地図タイプのオブジェクト地図 >> 。 それでは、この要件を実現するためのメソッドを作成しましょう。

public static Map<String, List<String>> aggregateByKeys(String filePath) {
    Map<String, List<String>> map = new HashMap<>();
    try (Stream<String> lines = Files.lines(Paths.get(filePath))) {
        lines.filter(line -> line.contains(":"))
          .forEach(line -> {
              String[] keyValuePair = line.split(":", 2);
              String key = keyValuePair[0];
              String value = keyValuePair[1];
              if (map.containsKey(key)) {
                  map.get(key).add(value);
              } else {
                  map.put(key, Stream.of(value).collect(Collectors.toList()));
              }
          });
    } catch (IOException e) {
        e.printStackTrace();
    }
    return map;
}

Stream アプローチを使用して、入力ファイルのすべての行を読み取りました。 実装は非常に簡単です。 入力行からキーと値を解析したら、結果のmapオブジェクトにキーがすでに存在するかどうかを確認します。 存在する場合は、既存のリストに値を追加します。 そうでなければ、私たちはリストを初期化する現在の値を単一の要素として含む: Stream.of(value).collect(Collectors.toList())。 

Collections.singletonList(value)または List.of(value)を使用してListを初期化するべきではないことを言及する価値があります。 それの訳は Collections.singletonListメソッドとList.of(Java 9以降)メソッドはどちらも不変のリストを返します。 つまり、同じキーが再び来た場合、その値をリストに追加することはできません。

次に、メソッドをテストして、それが機能するかどうかを確認しましょう。 いつものように、最初に期待される結果を作成します。

private static final Map<String, List<String>> EXPECTED_MAP_AGGREGATE = Stream.of(new String[][]{
      {"title", "The Lord of the Rings: The Return of the King"},
      {"director", "Peter Jackson"},
      {"actor", "Sean Astin", "Ian McKellen"}
  }).collect(Collectors.toMap(arr -> arr[0], arr -> Arrays.asList(Arrays.copyOfRange(arr, 1, arr.length))));

次に、テスト方法自体は非常に単純です。

@Test
public void givenInputFile_whenInvokeAggregateByKeys_shouldGetExpectedMap() {
    Map<String, List<String>> mapAgg = FileToHashMap.aggregateByKeys(filePath);
    assertThat(mapAgg).isEqualTo(EXPECTED_MAP_AGGREGATE);
}

テストを実行すると、テストに合格します。 これは、ソリューションが期待どおりに機能することを意味します。

7. 結論

この記事では、テキストファイルからコンテンツを読み取り、それをJava Mapオブジェクトに保存する2つの方法を学びました。BufferedReaderクラスを使用する方法とStream

さらに、重複キーを処理するための3つの戦略の実装、つまり、上書き、破棄、および集約について説明しました。

いつものように、コードのフルバージョンはGitHubから入手できます。