3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Javaで書いてたアレってScalaでどう書くの?(List→Map編)

Posted at

関数型でオブジェクト指向なScalaですが、「動けばいい」というコードであれば使う分には特に困らないと思っています1。関数型っぽく書くことにこだわりすぎて、そのコードを見る人に威圧感を与えてしまうと、せっかく開発者の裾野を広めようとしてもうまくいかないことも多いと考えています。
とは言え、ScalaでJavaっぽく書いてもある程度の恩恵は受けられますが、varやmutableを使わずに済ませられるならそれに越したことは無いと思うのです。

個人的に、Javaでの実装をScalaで書くケースをパターン化しておいて当てはめることをしているのですが、それが誰かの役にたてば良いなぁと思いまして、思いつく度に書いていこうと思った次第です。よろしければお付き合いください。

1. List[要素]からMap[key, 要素]のパターン

例えばマスタ系のデータなんかをid値をKeyにしたMapでキャッシュのように使用することを私はよくやります。

Javaの場合

要素(顧客)
public class Customer {
  /** 顧客ID. */
  public Long id;
  /** 顧客名. */
  public String name;
  /** 年齢. */
  public Integer age;
}
対象List
//本来customerListはDB等から取得するハズです
List<Customer> customerList = new ArrayList<Customer>(){{
    add(new Customer(){{
      this.id = 5L;this.name = "片岡";this.age = 39;
    }});
    add(new Customer(){{
      this.id = 3L;this.name = "青山";this.age = 60;
    }});
    add(new Customer(){{
      this.id = 1L;this.name = "藤島";this.age = 23;
    }});
}};
List→Map変換処理
Map<Long, Customer> customerMap = new HashMap<>();
for(Customer customer : customerList) {
  customerMap.put(customer.id, customer);
}

これでkeyが顧客ID、valueがCustomerインスタンスのcustomerMapが出来上がります。

Scalaの場合

要素(顧客)
/**
  * 顧客.
  * @param id 顧客ID
  * @param name 顧客名
  * @param age 年齢
  */
case class Customer(id:Long, name:String, age:Int)
対象List
//本来customerListはDB等から取得するハズです
val customerList = Seq(
  Customer(id=5L, name="片岡", age=39),
  Customer(id=3L, name="青山", age=60),
  Customer(id=1L, name="藤島", age=23)
)
List→Map変換処理
val customerMap = (customerList map (t => t.id -> t)).toMap

Seq[Customer] からmap操作でSeq[(Long, Customer)]2 が作成され、
さらにtoMapでMap[Long,Customer] に変換されるイメージです。
生成されるMapはimmutableなMapなので、変更されることを心配する事はありません。

もちろん、mutableなMapを使用することでJavaのように記述することも可能ですが、こちらの方がListからMapを生成しているんだな、と理解しやすいのではないでしょうか。
空のMapインスタンスを生成する処理、そのMapに要素を追加する処理を記述しなくて済むため、コーディングミスも減らせると考えられます3

2. List[要素]からMap[key,List[要素]]のパターン

例えば、1回のSQLで取得した、複数の請求IDに紐づく請求明細Listを元に、
key:請求ID
value:請求IDに紐づく請求明細List
のMapを生成するような時はこんな感じになります。

Javaの場合

要素(請求明細)
/**
 * 請求明細.
 */
public class BillingStatement {
  /** 請求明細ID(PK). */
  public Long id;
  /** 請求ID(FK). */
  public Long billingId;
  /** 商品名. */
  public String itemName;

  //数量とか単価とかあると思いますが、省略
}
対象List
//本来billingStatementListはDB等から取得するハズです
List<BillingStatement> billingStatementList = new ArrayList<BillingStatement>(){{
    add(new BillingStatement(){{
      this.id = 1L;this.billingId = 1L;
      this.itemName = "請求1 商品1";
    }});
    add(new BillingStatement(){{
      this.id = 2L;this.billingId = 1L;
      this.itemName = "請求1 商品2";
    }});
    add(new BillingStatement(){{
      this.id = 3L;this.billingId = 2L;
      this.itemName = "請求2 商品1";
    }});
    add(new BillingStatement(){{
      this.id = 4L;this.billingId = 3L;
      this.itemName = "請求3 商品1";
    }});
    add(new BillingStatement(){{
      this.id = 5L;this.billingId = 3L;
      this.itemName = "請求3 商品2";
    }});
    add(new BillingStatement(){{
      this.id = 6L;this.billingId = 3L;
      this.itemName = "請求3 商品3";
    }});
}};
List→Map変換処理
Map<Long, List<BillingStatement>> billingStatementsMap = new HashMap<>();
for(BillingStatement billingStatement : billingStatementList) {
  List<BillingStatement> billingStatements = billingStatementsMap.get(billingStatement.billingId);
  if(billingStatements == null) {
    billingStatements = new ArrayList<>();
    billingStatementsMap.put(billingStatement.billingId, billingStatements);
  }

  billingStatements.add(billingStatement);
}

これでkeyが請求ID、valueが請求IDに紐づく請求明細ListのMapが出来上がります。請求IDに紐づくListが存在しない時の処理...うっかりバグを組み込みそうな気がしませんか?

Scalaの場合

要素(請求明細)
/**
  * 請求明細.
  * 数量とか単価とかあると思いますが、省略
  * @param id 請求明細ID(PK)
  * @param billingId 請求ID(FK)
  * @param itemName 商品名
  */
case class BillingStatement(id:Long, billingId:Long, itemName:String)
対象List
//本来billingStatementListはDB等から取得するハズです
val billingStatementList = Seq(
  BillingStatement(id=1L, billingId=1L, itemName="請求1 商品1"),
  BillingStatement(id=2L, billingId=1L, itemName="請求1 商品2"),
  BillingStatement(id=3L, billingId=2L, itemName="請求2 商品1"),
  BillingStatement(id=4L, billingId=3L, itemName="請求3 商品1"),
  BillingStatement(id=5L, billingId=3L, itemName="請求3 商品2"),
  BillingStatement(id=6L, billingId=3L, itemName="請求3 商品3")
)
List→Map変換処理
val billingStatementsMap = billingStatementList.foldLeft(Map[Long, Seq[BillingStatement]]()) { (map, value) => {
  val key = value.billingId
  map.updated(key, map.getOrElse(key, Seq()) :+ value)
}}

生成されるMapはimmutableなMapです。安心。
map.getOrElse(key, Seq())は、
mapに指定したkeyに紐づくvalueが登録されていればその値、登録されていなければ空のSeqを返します。
そのSeqに対して要素valueを追加し、keyに紐づく要素を更新した新しいMapを返します。
要素の加算だけに使うイメージがあったfoldLeftも使い方ひとつでバグを作り込むことを回避できます4

まとめ

注意深くコーディングすることでバグやミスを減らすことはできます。ですが、人は間違いを犯す生き物です。
多くの変数、それに対する操作を組み合わせて結果を取得する場合、「うっかり」バグを埋め込んでしまう確率が高くなると感じています。
開発言語から提供されている機能を活用することで、その「注意深くする」為の時間・集中力を「人でなければできないこと」の方に費やしてみては如何でしょうか。


  1. もちろんFWを作ろうとか、プロジェクトの基底クラスを作ろうとしている場合はそれだと困るかもしれませんけど...

  2. 要素がTuple2のSeq

  3. ちなみにちょっと前までは、Map.newBuilderに対して要素をaddしてresultする処理をブロックスコープに書いてました

  4. 1回のループで複数の結果を返したい時は、Tupleを渡せばOKです

3
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?