Implementación de coincidencia de patrones en Java

Muchos idiomas modernos admiten la coincidencia de patrones a nivel de idioma. Actualmente, Java no admite la coincidencia de patrones, pero existe la esperanza de que las cosas puedan cambiar en el futuro.


La comparación con la muestra revela al desarrollador la capacidad de escribir código de manera más flexible y hermosa, mientras lo deja comprensible.


Usando las capacidades de Java 8, puede implementar algunas de las características de coincidencia de patrones en forma de biblioteca. En este caso, puede usar tanto la declaración como la expresión.


El patrón constante permite verificar la igualdad con las constantes. En Java, un conmutador le permite verificar números iguales, enumeraciones y cadenas. Pero a veces quiero verificar la igualdad de las constantes de los objetos utilizando 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); }; 

Para diferentes opciones de ramificación, debe usar las sobrecargas del método coincide (). Por lo tanto, en este momento, la biblioteca admite la capacidad de verificar los valores de una variable con 6 constantes.


 import org.kl.state.Else; import org.kl.state.Null; import static org.kl.pattern.ConstantPattern.matches; matches(data, 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) ); 

El patrón de tupla le permite verificar la igualdad de varios cambios con constantes simultáneamente.


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

Por el momento, la biblioteca admite la capacidad de especificar 4 variables y 6 ramas.


 import org.kl.state.Else; import org.kl.state.Null; import static org.kl.pattern.TuplePattern.matches; matches(side, width, "top", 25, () -> System.out.println("top"); "bottom", 30, () -> System.out.println("bottom"); "left", 15, () -> System.out.println("left"); "right", 15, () -> System.out.println("right"); Null.class, () -> System.out.println("Null value"), Else.class, () -> System.out.println("Default value") ); 

El patrón de prueba de tipo le permite hacer coincidir simultáneamente el tipo y extraer el valor de la variable. En Java, para esto necesitamos primero verificar el tipo, convertir al tipo y luego asignarlo a una nueva variable.


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

Por el momento, la biblioteca admite la capacidad de verificar los valores de una variable con 6 tipos.


 import org.kl.state.Else; import org.kl.state.Null; import static org.kl.pattern.VerifyPattern.matches; matches(data, 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); }; 

Por el momento, la biblioteca admite la capacidad de verificar los valores de una variable con 6 tipos y condiciones.


 import org.kl.state.Else; import org.kl.state.Null; import static org.kl.pattern.GuardPattern.matches; matches(data, 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); } ); 

Para simplificar la escritura de la condición, el desarrollador puede usar las siguientes funciones para la comparación: lessThan / lt, GreaterThan / gt, lessThanOrEqual / le, GreaterThanOrEqual / ge,
igual / eq, no igual / ne. Y para omitir las condiciones, puede intentar: siempre / sí, nunca / no.


 matches(data, 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 hacer coincidir simultáneamente el tipo y descomponer el objeto en componentes. En Java, para esto, primero debemos verificar el tipo, convertirlo a un tipo, asignarlo a una nueva variable y solo luego acceder a los campos de la clase a través de getters.


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

Actualmente, la biblioteca admite la capacidad de verificar los valores de una variable con 3 tipos y descomponer el objeto en 3 componentes.


 import org.kl.state.Else; import static org.kl.pattern.DeconstructPattern.matches; import static org.kl.pattern.DeconstructPattern.foreach; import static org.kl.pattern.DeconstructPattern.let; Figure figure = new Rectangle(); let(figure, (int w, int h) -> { System.out.println("border: " + w + " " + h)); }); matches(figure, 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 un componente, una clase debe tener uno o más métodos de deconstrucción. Estos métodos deben etiquetarse con Extracto de anotación.
Todos los parámetros deben estar abiertos. Dado que las primitivas no se pueden pasar a un método por referencia, debe usar envoltorios para las primitivas IntRef, FloatRef, etc.


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

También utilizando Java 11, puede inferir tipos de parámetros de deconstrucción.


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

El patrón de propiedad le permite hacer coincidir simultáneamente el tipo y acceder a los campos de la clase por sus nombres. En lugar de proporcionar todos los campos, puede acceder a los campos que necesita en cualquier orden.


 let (w: int w, h:int h) = figure; switch (figure) { case Rect(w: int w == 5, h: int h == 10) -> out.println("sqr: " + (w * h)); case Rect(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)); } 

Por el momento, la biblioteca admite la capacidad de verificar los valores de una variable con 3 tipos y descomponer el objeto en 3 componentes.


 import org.kl.state.Else; import static org.kl.pattern.PropertyPattern.matches; import static org.kl.pattern.PropertyPattern.foreach; import static org.kl.pattern.PropertyPattern.let; import static org.kl.pattern.PropertyPattern.of; Figure figure = new Rectangle(); let(figure, of("w", "h"), (int w, int h) -> { System.out.println("border: " + w + " " + h)); }); matches(figure, 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 usar otro método para simplificar los campos de nomenclatura.


 Figure figure = new Rect(); let(figure, Rect::w, Rect::h, (int w, int h) -> { System.out.println("border: " + w + " " + h)); }); matches(figure, Rect.class, Rect::w, Rect::h, (int w, int h) -> out.println("sqr: " + (w * h)), Circle.class, Circle::r, (int r) -> 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 el valor de los campos en el orden de declaración. En Java, para esto, primero debemos verificar el tipo, convertirlo a un tipo, asignarlo a una nueva variable, y solo luego obtener los campos de la clase y verificar la igualdad.


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

Por el momento, la biblioteca admite la capacidad de verificar los valores de una variable con 6 tipos y verificar 4 campos a la vez.


 import org.kl.state.Else; import org.kl.state.Null; import static org.kl.pattern.PositionPattern.matches; import static org.kl.pattern.PositionPattern.of; matches(data, 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 verificar algunos campos, estos campos deben marcarse con anotaciones @Exclude. 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 simultáneamente el tipo y deconstruir el objeto utilizando métodos de fábrica.


 Optional some = ...; switch (some) { case Optional.empty() -> System.out.println("empty value"); case Optional.of(var v) -> System.out.println("value: " + v); default -> System.out.println("Default value"); }; 

Por el momento, la biblioteca admite la capacidad de verificar los valores de una variable con 6 tipos y descomponer el objeto en 3 componentes.


 import org.kl.state.Else; import static org.kl.pattern.StaticPattern.matches; import static org.kl.pattern.StaticPattern.of; Optional some = ...; matches(figure, Optinal.class, of("empty"), () -> System.out.println("empty value"), Optinal.class, of("of") , (var v) -> System.out.println("value: " + v), Else.class, () -> System.out.println("Default value") ); 

Al mismo tiempo, para obtener el componente, la clase debe tener uno o más métodos de deconstrucción, etiquetados como anotaciones de extracción .


 @Extract public void of(IntRef value) { value.set(this.value); } 

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


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

Usando la biblioteca, puede escribir de la siguiente manera.


 import org.kl.state.Else; import org.kl.range.Ranges; import static org.kl.pattern.SequencePattern.matches; List<Integer> list = ...; matches(figure, Ranges.head(), (var h) -> System.out.println("list head: " + h), Ranges.tail(), (var t) -> System.out.println("list tail: " + t), Ranges.empty() () -> System.out.println("Empty value"), Else.class, () -> System.out.println("Default value") ); 

Además, para simplificar el código, puede usar las siguientes funciones.


 import static org.kl.pattern.CommonPattern.with; import static org.kl.pattern.CommonPattern.when; Rectangle rect = 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") ); 

Como puede ver, la coincidencia de patrones es una herramienta poderosa que hace que escribir código sea mucho más fácil. Usando expresiones lambda, referencias de métodos y la derivación de tipos de parámetros lambda, puede emular las posibilidades de coincidencia de patrones por los propios medios del lenguaje.


El código fuente de la biblioteca está abierto y disponible en github .

Source: https://habr.com/ru/post/460773/


All Articles