CorrespondĂȘncia de padrĂ”es em Java 8

Muitos idiomas modernos suportam correspondĂȘncia de padrĂ”es no nĂ­vel do idioma.



A linguagem Java nĂŁo Ă© exceção. E o Java 16 adicionarĂĄ suporte para correspondĂȘncia de padrĂ”es para o operador instanceof como um recurso final.



Esperançosamente, no futuro, a correspondĂȘncia de padrĂ”es serĂĄ estendida a outras construçÔes de linguagem.



A correspondĂȘncia de padrĂ”es dĂĄ ao desenvolvedor a capacidade de escrever cĂłdigo de forma mais flexĂ­vel e bonita, ao mesmo tempo que o mantĂ©m compreensĂ­vel.



Mas e se vocĂȘ nĂŁo puder mudar de um motivo ou outro para novas versĂ”es do Java. Felizmente, usando os recursos do Java 8, vocĂȘ pode implementar alguns dos recursos de correspondĂȘncia de padrĂ”es na forma de uma biblioteca.



Vejamos alguns padrÔes e como eles podem ser implementados usando uma biblioteca simples.



O padrĂŁo constante permite que vocĂȘ verifique a igualdade com as constantes. Em Java, a instrução switch permite que vocĂȘ verifique a igualdade de nĂșmeros, enumeraçÔes e strings. Mas Ă s vezes vocĂȘ deseja verificar a igualdade das constantes do objeto usando o 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);
};

      
      





CĂłdigo semelhante pode ser escrito da seguinte maneira. Ao mesmo tempo, sob o capĂŽ, os valores sĂŁo comparados e verificados em uma instrução if. VocĂȘ pode usar um formulĂĄrio de instrução e uma expressĂŁo.



Também é muito fåcil trabalhar com faixas 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)
);

      
      





O padrĂŁo de tupla permite que vocĂȘ verifique a igualdade de vĂĄrias variĂĄveis ​​com constantes ao mesmo tempo.



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

      
      





Nesse caso, além de ser usado na forma de um switch, ele pode ser decomposto em pares ou passado sequencialmente em um loop.



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

      
      





O padrĂŁo de teste de tipo permite combinar o tipo e extrair o valor de uma variĂĄvel ao mesmo tempo.



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

      
      





Em Java, para isso precisamos primeiro verificar o tipo, lançar para o tipo e então atribuí-lo a uma nova variåvel. Esse padrão torna seu código muito mais 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); }
);

      
      





O padrão de proteção permite combinar o tipo e verificar as condiçÔes ao mesmo tempo.



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

      
      





Um design semelhante pode ser implementado da seguinte maneira. Para tornar mais fĂĄcil escrever condiçÔes, vocĂȘ pode usar as seguintes funçÔes de comparação: lessThan / lt, maiorThan / gt, lessThanOrEqual / le, maiorThanOrEqual / ge, equal / eq, notEqual / ne. E para omitir as condiçÔes, vocĂȘ pode alterar: sempre / sim, nunca / nĂŁo.



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

      
      





O padrão de desconstrução permite mapear simultaneamente um tipo e decompor um objeto em seus 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));
}

      
      





Em Java, para isso, primeiro precisamos verificar o tipo, converter para o tipo, atribuĂ­-lo a uma nova variĂĄvel e sĂł entĂŁo acessar os campos da classe por meio 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));
});

      
      





Além disso, para obter o componente, a classe deve ter um ou mais métodos de desconstrução. Esses métodos devem ser anotados Extrair...

Todos os parĂąmetros devem estar abertos. Como as primitivas nĂŁo podem ser passadas para um mĂ©todo por referĂȘncia, vocĂȘ precisa usar wrappers para IntRef, FloatRef, etc. primitivas.



Para reduzir a sobrecarga usando reflexão, cache e truques são usados ​​com a classe LambdaMetafactory padrão.



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

      
      





O padrĂŁo de propriedade permite que vocĂȘ combine simultaneamente o tipo e acesse os campos de classe por seus nomes.



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 Ă© uma forma simplificada do padrĂŁo de desconstrução, em que vocĂȘ sĂł precisa desconstruir campos de classe especĂ­ficos.



Para reduzir a sobrecarga usando reflexão, cache e truques são usados ​​com a classe LambdaMetafactory padrão.



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

      
      





VocĂȘ tambĂ©m pode usar outro mĂ©todo com referĂȘncias de mĂ©todo para simplificar a nomenclatura dos 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));
});

      
      







O padrĂŁo de posição permite que vocĂȘ corresponda simultaneamente o tipo e verifique os valores dos campos na ordem de declaração.



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

      
      





Em Java, para isso, primeiro precisamos verificar o tipo, converter para o tipo, atribuĂ­-lo a uma nova variĂĄvel e sĂł entĂŁo acessar os campos da classe por meio de getters e verificar a igualdade.

Para reduzir a sobrecarga usando reflexĂŁo, o cache Ă© usado.



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

      
      





AlĂ©m disso, se o desenvolvedor nĂŁo quiser validar alguns campos, esses campos devem ser marcados com anotaçÔes Excluir... Esses campos devem ser declarados por Ășltimo.



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

      
      





O padrĂŁo estĂĄtico permite que vocĂȘ combine o tipo e desconstrua um objeto simultaneamente usando 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")
};

      
      





Semelhante ao padrão de desconstrução, mas o nome dos métodos de desconstrução que são anotados Extrairdeve ser especificado explicitamente.



Para reduzir a sobrecarga usando reflexão, cache e truques são usados ​​com a classe LambdaMetafactory padrão.



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

      
      





O padrĂŁo de sequĂȘncia torna mais fĂĄcil processar sequĂȘncias de dados.



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, vocĂȘ pode simplesmente trabalhar com sequĂȘncias de dados.



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

      
      





AlĂ©m disso, para simplificar o cĂłdigo, vocĂȘ pode usar as funçÔes a seguir, que podem ser vistas em linguagens modernas como recursos ou funçÔes de linguagem.



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 vocĂȘ pode ver, a correspondĂȘncia de padrĂ”es Ă© uma ferramenta poderosa que torna a escrita de cĂłdigo muito mais fĂĄcil. Usando os recursos do Java 8, vocĂȘ pode emular os recursos de correspondĂȘncia de padrĂ”es pelos prĂłprios meios da linguagem.



O código-fonte da biblioteca pode ser visualizado no github: link . Eu ficaria feliz em receber comentårios e sugestÔes para melhorias.



All Articles