Correspondance de modèles dans Java 8

De nombreuses langues modernes prennent en charge la correspondance de modèles au niveau de la langue.



Le langage Java ne fait pas exception. Et Java 16 ajoutera la prise en charge de la correspondance de modèles pour l'opérateur instanceof en tant que fonctionnalité finale.



Espérons qu'à l'avenir, la correspondance de modèles sera étendue à d'autres constructions de langage.



La correspondance de modèles donne au développeur la possibilité d'écrire du code de manière plus flexible et plus jolie, tout en le gardant compréhensible.



Mais que faire si vous ne pouvez pas passer d'une raison ou d'une autre à de nouvelles versions de Java. Heureusement, en utilisant les capacités de Java 8, vous pouvez implémenter certaines des capacités de correspondance de modèles sous la forme d'une bibliothèque.



Regardons quelques modèles et comment ils peuvent être implémentés à l'aide d'une simple bibliothèque.



Le modèle constant vous permet de vérifier l'égalité avec les constantes. En Java, l'instruction switch vous permet de vérifier l'égalité des nombres, des énumérations et des chaînes. Mais parfois, vous souhaitez vérifier l'égalité des constantes d'objet à l'aide de la méthode 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);
};

      
      





Un code similaire peut être écrit comme suit. En même temps, sous le capot, les valeurs sont comparées et vérifiées dans une instruction if. Vous pouvez utiliser à la fois un formulaire d'instruction et une expression.



Il est également très facile de travailler avec des plages de valeurs.



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

      
      





Le modèle de tuple vous permet de vérifier l'égalité de plusieurs variables avec des constantes en même temps.



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

      
      





Dans ce cas, en plus d'être utilisé sous la forme d'un interrupteur, il peut être décomposé en ceux correspondants ou passé séquentiellement dans une boucle.



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

      
      





Le modèle de test de type vous permet de faire correspondre le type et d'extraire la valeur d'une variable en même temps.



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, pour cela, nous devons d'abord vérifier le type, convertir le type, puis l'assigner à une nouvelle variable. Ce modèle rend votre code beaucoup plus facile.



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

      
      





Le modèle de garde vous permet de faire correspondre le type et de vérifier les conditions en même temps.



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

      
      





Une conception similaire peut être mise en œuvre comme suit. Pour faciliter l'écriture des conditions, vous pouvez utiliser les fonctions de comparaison suivantes: lessThan / lt, greaterThan / gt, lessThanOrEqual / le, greaterThanOrEqual / ge, equal / eq, notEqual / ne. Et pour omettre les conditions, vous pouvez changer: toujours / oui, jamais / non.



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

      
      





Le modèle de déconstruction vous permet de mapper simultanément un type et de décomposer un objet en ses composants.



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, pour cela, nous devons d'abord vérifier le type, convertir le type, l'affecter à une nouvelle variable, puis accéder aux champs de classe via des 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));
});

      
      





De plus, pour obtenir le composant, la classe doit avoir une ou plusieurs méthodes de déconstruction. Ces méthodes doivent être annotées Extrait...

Tous les paramètres doivent être ouverts. Comme les primitives ne peuvent pas être passées à une méthode par référence, vous devez utiliser des wrappers pour les primitives IntRef, FloatRef, etc.



Pour réduire la surcharge en utilisant la réflexion, la mise en cache et les astuces sont utilisées avec la classe LambdaMetafactory standard.



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

      
      





Le modèle de propriété vous permet de faire correspondre simultanément le type et d'accéder aux champs de classe par leurs noms.



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

      
      





Il s'agit d'une forme simplifiée du modèle de déconstruction, où il vous suffit de déconstruire des champs de classe spécifiques.



Pour réduire la surcharge en utilisant la réflexion, la mise en cache et les astuces sont utilisées avec la classe LambdaMetafactory standard.



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

      
      





Vous pouvez également utiliser une autre méthode avec des références de méthode pour simplifier la dénomination des champs.



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

      
      







Le modèle de position vous permet de faire correspondre simultanément le type et de vérifier les valeurs des champs dans l'ordre de déclaration.



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, pour cela, nous devons d'abord vérifier le type, convertir le type, l'affecter à une nouvelle variable, puis accéder aux champs de classe via des getters et vérifier l'égalité.

Pour réduire la surcharge en utilisant la réflexion, la mise en cache est utilisée.



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

      
      





De plus, si le développeur ne souhaite pas valider certains champs, ces champs doivent être marqués d'annotations Exclure... Ces champs doivent être déclarés en dernier.



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

      
      





Le modèle statique vous permet de faire correspondre simultanément le type et de déconstruire un objet à l'aide de méthodes d'usine.



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

      
      





Similaire au modèle de déconstruction, mais le nom des méthodes de déconstruction annotées Extraitdoit être spécifié explicitement.



Pour réduire la surcharge en utilisant la réflexion, la mise en cache et les astuces sont utilisées avec la classe LambdaMetafactory standard.



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

      
      





Le modèle de séquence facilite le traitement des séquences de données.



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

      
      





En utilisant des méthodes de bibliothèque, vous pouvez simplement travailler avec des séquences de données.



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

      
      





De plus, pour simplifier le code, vous pouvez utiliser les fonctions suivantes, qui peuvent être vues dans les langues modernes comme des fonctionnalités ou des fonctions de langage.



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

      
      





Comme vous pouvez le voir, la correspondance de modèles est un outil puissant qui facilite beaucoup l'écriture de code. En utilisant les capacités de Java 8, vous pouvez émuler les capacités de correspondance de modèles par le biais même du langage.



Le code source de la bibliothèque peut être consulté sur github: link . Je serais heureux de recevoir des commentaires et des suggestions d'amélioration.



All Articles