Java8でのパターンマッチング

多くの現代言語は、言語レベルでのパターンマッチングをサポートしています。



Java言語も例外ではありません。また、Java 16では、最後の機能として、instanceof演算子のパターンマッチングのサポートが追加されます。



うまくいけば、将来的には、パターンマッチングが他の言語構造に拡張されるでしょう。



パターンマッチングにより、開発者はコードを理解しやすくしながら、より柔軟かつきれいにコードを記述できるようになります。



しかし、何らかの理由でJavaの新しいバージョンに切り替えることができない場合はどうでしょうか。幸い、Java 8の機能を使用すると、パターンマッチング機能の一部をライブラリの形式で実装できます。



いくつかのパターンと、単純なライブラリを使用してそれらを実装する方法を見てみましょう。



定数パターンを使用すると、定数との同等性を確認できます。 Javaでは、switchステートメントを使用すると、数値、列挙、および文字列が等しいかどうかを確認できます。ただし、equals()メソッドを使用してオブジェクト定数が等しいかどうかを確認したい場合があります。



switch (data) {
      case new Person("man")    -> System.out.println("man");
      case new Person("woman")  -> System.out.println("woman");
      case new Person("child") 	-> System.out.println("child");        
      case null                 -> System.out.println("Null value ");
      default                   -> System.out.println("Default value: " + data);
};

      
      





同様のコードは次のように書くことができます。同時に、内部では、値が比較され、ifステートメントでチェックされます。ステートメントフォームと式の両方を使用できます。



また、値の範囲を操作するのも非常に簡単です。



import static org.kl.jpml.pattern.ConstantPattern.*;

matches(data).as(
      new Person("man"),    () ->  System.out.println("man"),
      new Person("woman"),  () ->  System.out.println("woman"),
      new Person("child"),  () ->  System.out.println("child"),       
      Null.class,           () ->  System.out.println("Null value "),
      Else.class,           () ->  System.out.println("Default value: " + data)
);

matches(data).as(
      or(1, 2),    () ->  System.out.println("1 or 2"),
      in(3, 6),    () ->  System.out.println("between 3 and 6"),
      in(7),       () ->  System.out.println("7"),        
      Null.class,  () ->  System.out.println("Null value "),
      Else.class,  () ->  System.out.println("Default value: " + data)
);

      
      





タプルパターンを使用すると、定数を使用して複数の変数が同時に等しいかどうかを確認できます。



var (side, width) = border;

switch (side, width) {
      case ("top",    25) -> System.out.println("top");
      case ("bottom", 30) -> System.out.println("bottom");
      case ("left",   15) -> System.out.println("left");        
      case ("right",  15) -> System.out.println("right"); 
      case null         -> System.out.println("Null value ");
      default           -> System.out.println("Default value ");
};

for ((side, width) : listBorders) {
      System.out.println("border: " + [side + "," + width]); 	
}

      
      





この場合、スイッチの形で使用されることに加えて、それは一致するものに分解されるか、またはループで順番に渡されることができます。



import static org.kl.jpml.pattern.TuplePattern.*;

let(border, (String side, int width) -> {
    System.out.println("border: " + side + "," + width);
});

matches(side, width).as(
      of("top",    25),  () -> System.out.println("top"),
      of("bottom", 30),  () -> System.out.println("bottom"),
      of("left",   15,  () -> System.out.println("left"),       
      of("right",  15),  () -> System.out.println("right"),         
      Null.class,    () -> System.out.println("Null value"),
      Else.class,    () -> System.out.println("Default value")
);

foreach(listBorders, (String side, int width) -> {
     System.out.println("border: " + side + "," + width); 	
}

      
      





型テストパターンを使用すると、型を照合すると同時に変数の値を抽出できます。



switch (data) {
      case Integer i  -> System.out.println(i * i);
      case Byte    b  -> System.out.println(b * b);
      case Long    l  -> System.out.println(l * l);        
      case String  s  -> System.out.println(s * s);
      case null       -> System.out.println("Null value ");
      default         -> System.out.println("Default value: " + data);
};

      
      





Javaでは、このために、最初に型をチェックし、型にキャストしてから、それを新しい変数に割り当てる必要があります。このパターンにより、コードがはるかに簡単になります。



import static org.kl.jpml.pattern.VerifyPattern.matches;

matches(data).as(
      Integer.class, i  -> { System.out.println(i * i); },
      Byte.class,    b  -> { System.out.println(b * b); },
      Long.class,    l  -> { System.out.println(l * l); },
      String.class,  s  -> { System.out.println(s * s); },
      Null.class,    () -> { System.out.println("Null value "); },
      Else.class,    () -> { System.out.println("Default value: " + data); }
);

      
      





ガードパターンにより、タイプのマッチングと条件のチェックを同時に行うことができます。



switch (data) {
      case Integer i && i != 0     -> System.out.println(i * i);
      case Byte    b && b > -1     -> System.out.println(b * b);
      case Long    l && l < 5      -> System.out.println(l * l);
      case String  s && !s.empty() -> System.out.println(s * s);
      case null                    -> System.out.println("Null value ");
      default                      -> System.out.println("Default: " + data);
};

      
      





同様の設計は、次のように実装できます。条件の記述を容易にするために、次の比較関数を使用できます:lessThan / lt、greaterThan / gt、lessThanOrEqual / le、greaterThanOrEqual / ge、equal / eq、notEqual / ne。そして、条件を省略するために、あなたは変えることができます:常に/はい、決して/いいえ。



import static org.kl.jpml.pattern.GuardPattern.matches;

matches(data).as(           
      Integer.class, i  -> i != 0,  i  -> { System.out.println(i * i); },
      Byte.class,    b  -> b > -1,  b  -> { System.out.println(b * b); },
      Long.class,    l  -> l == 5,  l  -> { System.out.println(l * l); },
      Null.class,    () -> { System.out.println("Null value "); },
      Else.class,    () -> { System.out.println("Default value: " + data); }
);

matches(data).as(           
      Integer.class, ne(0),  i  -> { System.out.println(i * i); },
      Byte.class,    gt(-1), b  -> { System.out.println(b * b); },
      Long.class,    eq(5),  l  -> { System.out.println(l * l); },
      Null.class,    () -> { System.out.println("Null value "); },
      Else.class,    () -> { System.out.println("Default value: " + data); }
);

      
      





分解パターンを使用すると、タイプをマップすると同時に、オブジェクトをそのコンポーネントに分解できます。



let (int w, int h) = figure;
 
switch (figure) {
      case Rectangle(int w, int h) -> out.println("square: " + (w * h));
      case Circle   (int r)        -> out.println("square: " + (2 * Math.PI * r));
      default                      -> out.println("Default square: " + 0);
};
   
for ((int w, int h) :  listFigures) {
      System.out.println("square: " + (w * h));
}

      
      





Javaでは、このために、最初に型をチェックし、型にキャストし、それを新しい変数に割り当ててから、ゲッターを介してクラスフィールドにアクセスする必要があります。



import static org.kl.jpml.pattern.DeconstructPattern.*;

Figure figure = new Rectangle();

let(figure, (int w, int h) -> {
      System.out.println("border: " + w + " " + h));
});

matches(figure).as(
      Rectangle.class, (int w, int h) -> out.println("square: " + (w * h)),
      Circle.class,    (int r)        -> out.println("square: " + (2 * Math.PI * r)),
      Else.class,      ()             -> out.println("Default square: " + 0)
);
   
foreach(listRectangles, (int w, int h) -> {
      System.out.println("square: " + (w * h));
});

      
      





さらに、コンポーネントを取得するには、クラスに1つ以上の分解メソッドが必要です。これらのメソッドには注釈を付ける必要があります エキス..。

すべてのパラメータが開いている必要があります。プリミティブは参照によってメソッドに渡すことができないため、IntRef、FloatRefなどのプリミティブのラッパーを使用する必要があります。



リフレクションを使用してオーバーヘッドを削減するために、標準のLambdaMetafactoryクラスでキャッシュとトリックが使用されます。



@Extract
public void deconstruct(IntRef width, IntRef height) {
      width.set(this.width);
      height.set(this.height);
 }

      
      





プロパティパターンを使用すると、タイプを一致させると同時に、名前でクラスフィールドにアクセスできます。



let (w: int w, h:int h) = figure;
 
switch (figure) {
      case Rectangle(w: int w == 5,  h: int h == 10) -> out.println("sqr: " + (w * h));
      case Rectangle(w: int w == 10, h: int h == 15) -> out.println("sqr: " + (w * h));
      case Circle   (r: int r) -> out.println("sqr: " + (2 * Math.PI * r));
      default                  -> out.println("Default sqr: " + 0);
};
   
for ((w: int w, h: int h) :  listRectangles) {
      System.out.println("square: " + (w * h));
}

      
      





これは、特定のクラスフィールドを分解するだけでよい、分解パターンの簡略化された形式です。



リフレクションを使用してオーバーヘッドを削減するために、標準のLambdaMetafactoryクラスでキャッシュとトリックが使用されます。



import static org.kl.jpml.pattern.PropertyPattern.*;  

Figure figure = new Rectangle();

let(figure, of("w", "h"), (int w, int h) -> {
      System.out.println("border: " + w + " " + h));
});

matches(figure).as(
      Rect.class,    of("w", 5,  "h", 10), (int w, int h) -> out.println("sqr: " + (w * h)),
      Rect.class,    of("w", 10, "h", 15), (int w, int h) -> out.println("sqr: " + (w * h)),
      Circle.class,  of("r"), (int r)  -> out.println("sqr: " + (2 * Math.PI * r)),
      Else.class,    ()                -> out.println("Default sqr: " + 0)
);
   
foreach(listRectangles, of("x", "y"), (int w, int h) -> {
      System.out.println("square: " + (w * h));
});

      
      





メソッド参照で別のメソッドを使用して、フィールドの名前を単純化することもできます。



Figure figure = new Rect();

let(figure, Rect::w, Rect::h, (int w, int h) -> {
      System.out.println("border: " + w + " " + h));
});

matches(figure).as(
      Rect.class,    Rect::w, Rect::h, (int w, int h) -> System.out.println("sqr: " + (w * h)),
      Circle.class,  Circle::r, (int r)  -> System.out.println("sqr: " + (2 * Math.PI * r)),
      Else.class,    ()                  -> System.out.println("Default sqr: " + 0)
);
   
foreach(listRectangles, Rect::w, Rect::h, (int w, int h) -> {
      System.out.println("square: " + (w * h));
});

      
      







位置パターンを使用すると、タイプを一致させると同時に、宣言の順序でフィールド値を確認できます。



switch (data) {
      case Circle(5)   -> System.out.println("small circle");
      case Circle(15)  -> System.out.println("middle circle");
      case null        -> System.out.println("Null value ");
      default          -> System.out.println("Default value: " + data);
};

      
      





Javaでは、このために、最初に型をチェックし、型にキャストし、それを新しい変数に割り当ててから、ゲッターを介してクラスフィールドにアクセスし、等しいかどうかを確認する必要があります。

リフレクションを使用してオーバーヘッドを削減するために、キャッシュが使用されます。



import static org.kl.jpml.pattern.PositionPattern.*;

matches(data).as(           
      Circle.class,  of(5),  () -> { System.out.println("small circle"); },
      Circle.class,  of(15), () -> { System.out.println("middle circle"); },
      Null.class,            () -> { System.out.println("Null value "); },
      Else.class,            () -> { System.out.println("Default value: " + data); }
);

      
      





また、開発者が一部のフィールドを検証したくない場合は、これらのフィールドに注釈を付ける必要があります 除外する..。これらのフィールドは最後に宣言する必要があります。



class Circle {
      private int radius;
      	  
      @Exclude
      private int temp;
 }

      
      





静的パターンを使用すると、ファクトリメソッドを使用して、タイプの照合とオブジェクトの分解を同時に行うことができます。



 
switch (some) {
      case Result.value(var v) -> System.out.println("value: " + v)
      case Result.error(var e) -> System.out.println("error: " + e)
      default                    -> System.out.println("Default value")
};

      
      





分解パターンに似ていますが、注釈が付けられている分解メソッドの名前です エキス明示的に指定する必要があります。



リフレクションを使用してオーバーヘッドを削減するために、標準のLambdaMetafactoryクラスでキャッシュとトリックが使用されます。



import static org.kl.jpml.pattern.StaticPattern.*;

matches(figure).as(
      Result.class, of("value"), (var v) -> System.out.println("value: " + v),
      Result.class, of("error"), (var e) -> System.out.println("error: " + e),
      Else.class, () -> System.out.println("Default value")
); 

      
      





シーケンスパターンにより、データシーケンスの処理が容易になります。



List<Integer> list = ...;
  
switch (list) {
      case empty()     -> System.out.println("Empty value")
      case head(var h) -> System.out.println("list head: " + h)
      case tail(var t) -> System.out.println("list tail: " + t)         
      default          -> System.out.println("Default value")
};

      
      





ライブラリメソッドを使用すると、データシーケンスを簡単に操作できます。



import static org.kl.jpml.pattern.SequencePattern.*;

List<Integer> list = List.of(1, 2, 3);

matches(figure).as(
      empty() ()      -> System.out.println("Empty value"),
      head(), (var h) -> System.out.println("list head: " + h),
      tail(), (var t) -> System.out.println("list tail: " + t),      
      Else.class, ()  -> System.out.println("Default value")
);   

      
      





また、コードを単純化するために、次の関数を使用できます。これらの関数は、現代言語では言語の特徴または関数として表示されます。



import static org.kl.jpml.pattern.CommonPattern.*;

var rect = lazy(Rectangle::new);
var result = elvis(rect.get(), new Rectangle());
   
with(rect, it -> {
   it.setWidth(5);
   it.setHeight(10);
});
   
when(
    side == Side.LEFT,  () -> System.out.println("left  value"),
    side == Side.RIGHT, () -> System.out.println("right value")
);
   
repeat(3, () -> {
   System.out.println("three time");
)
   
int even = self(number).takeIf(it -> it % 2 == 0);
int odd  = self(number).takeUnless(it -> it % 2 == 0);

      
      





ご覧のとおり、パターンマッチングは、コードの記述を非常に簡単にする強力なツールです。Java 8の機能を使用すると、言語を使用してパターンマッチングの機能をエミュレートできます。



ライブラリのソースコードは、github:linkで表示できます 改善のためのフィードバックや提案をいただければ幸いです。



All Articles