Coincidencia de patrones en Java 8

Muchos idiomas modernos admiten la coincidencia de patrones a nivel de idioma.



El lenguaje Java no es una excepción. Y Java 16 agregará soporte para la coincidencia de patrones para el operador de instancia como una característica final.



Con suerte, en el futuro, la coincidencia de patrones se extenderá a otras construcciones del lenguaje.



La coincidencia de patrones le da al desarrollador la capacidad de escribir código de manera más flexible y bonita, mientras lo mantiene comprensible.



Pero, ¿qué pasa si no puede cambiar de una razón u otra a nuevas versiones de Java? Afortunadamente, utilizando las capacidades de Java 8, puede implementar algunas de las capacidades de coincidencia de patrones en forma de biblioteca.



Veamos algunos patrones y cómo se pueden implementar usando una biblioteca simple.



El patrón constante le permite verificar la igualdad con las constantes. En Java, la declaración de cambio le permite verificar la igualdad de números, enumeraciones y cadenas. Pero a veces desea verificar la igualdad de las constantes del objeto usando el método 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);
};

      
      





Se puede escribir un código similar de la siguiente manera. Al mismo tiempo, bajo el capó, los valores se comparan y verifican en una declaración if. Puede utilizar tanto una forma de declaración como una expresión.



También es muy fácil trabajar con rangos de valores.



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)
);

      
      





El patrón de tupla le permite verificar la igualdad de varias variables con constantes al mismo tiempo.



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]); 	
}

      
      





En este caso, además de utilizarse en forma de conmutador, se puede descomponer en otros que coincidan o pasar secuencialmente en un bucle.



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); 	
}

      
      





El patrón de prueba de tipo le permite hacer coincidir el tipo y extraer el valor de una variable al mismo tiempo.



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);
};

      
      





En Java, para esto, primero debemos verificar el tipo, convertirlo en el tipo y luego asignarlo a una nueva variable. Este patrón hace que su código sea mucho más fácil.



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); }
);

      
      





El patrón de protección le permite hacer coincidir simultáneamente el tipo y verificar las condiciones.



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);
};

      
      





Se puede implementar un diseño similar de la siguiente manera. Para facilitar la escritura de condiciones, puede utilizar las siguientes funciones de comparación: lessThan / lt, majorThan / gt, lessThanOrEqual / le, majorThanOrEqual / ge, equal / eq, notEqual / ne. Y para omitir las condiciones, puede cambiar: siempre / sí, nunca / no.



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); }
);

      
      





El patrón de deconstrucción le permite mapear simultáneamente un tipo y descomponer un objeto en sus componentes.



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));
}

      
      





En Java, para esto, primero debemos verificar el tipo, convertirlo al tipo, asignarlo a una nueva variable y solo luego acceder a los campos de la clase a través de getters.



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));
});

      
      





Además, para obtener el componente, la clase debe tener uno o más métodos de deconstrucción. Estos métodos deben anotarse Extraer...

Todos los parámetros deben estar abiertos. Dado que las primitivas no se pueden pasar a un método por referencia, es necesario utilizar envoltorios para las primitivas IntRef, FloatRef, etc.



Para reducir la sobrecarga mediante la reflexión, el almacenamiento en caché y los trucos se utilizan con la clase LambdaMetafactory estándar.



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

      
      





El patrón Propiedad le permite hacer coincidir simultáneamente el tipo y acceder a los campos de clase por sus nombres.



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));
}

      
      





Esta es una forma simplificada del patrón de deconstrucción, donde solo necesita deconstruir campos de clase específicos.



Para reducir la sobrecarga mediante la reflexión, el almacenamiento en caché y los trucos se utilizan con la clase estándar 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));
});

      
      





También puede utilizar otro método con referencias a métodos para simplificar la asignación de nombres a los campos.



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));
});

      
      







El patrón de posición le permite hacer coincidir simultáneamente el tipo y verificar los valores de campo en el orden de declaración.



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);
};

      
      





En Java, para esto, primero debemos verificar el tipo, convertirlo en el tipo, asignarlo a una nueva variable y solo luego acceder a los campos de la clase a través de getters y verificar la igualdad.

Para reducir la sobrecarga mediante la reflexión, se utiliza el almacenamiento en caché.



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); }
);

      
      





Además, si el desarrollador no desea validar algunos campos, estos campos deben marcarse con anotaciones. Excluir... Estos campos deben declararse en último lugar.



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

      
      





El patrón estático le permite hacer coincidir el tipo y deconstruir simultáneamente un objeto utilizando métodos de fábrica.



 
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")
};

      
      





Similar al patrón de deconstrucción, pero el nombre de los métodos de deconstrucción que están anotados Extraerdebe especificarse explícitamente.



Para reducir la sobrecarga mediante la reflexión, el almacenamiento en caché y los trucos se utilizan con la clase LambdaMetafactory estándar.



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")
); 

      
      





El patrón de secuencia facilita el procesamiento de secuencias de datos.



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")
};

      
      





Usando métodos de biblioteca, simplemente puede trabajar con secuencias de datos.



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")
);   

      
      





Además, para simplificar el código, puede utilizar las siguientes funciones, que se pueden ver en los lenguajes modernos como características o funciones del lenguaje.



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);

      
      





Como puede ver, la coincidencia de patrones es una herramienta poderosa que facilita mucho la escritura de código. Usando las capacidades de Java 8, puede emular las capacidades de la coincidencia de patrones por los propios medios del lenguaje.



El código fuente de la biblioteca se puede ver en github: link . Me encantaría recibir comentarios y sugerencias para mejorar.



All Articles