You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/05/01 15:34:59 UTC

[sis] 01/01: Merge branch 'geoapi-3.1': remove contravariance in filters and expressions. https://issues.apache.org/jira/browse/SIS-578

This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/sis.git

commit 410b3a1f6acefcbc61d5a45551482589661c6c97
Merge: ff2beab7ba 7d712579ff
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Mon May 1 16:29:33 2023 +0200

    Merge branch 'geoapi-3.1': remove contravariance in filters and expressions.
    https://issues.apache.org/jira/browse/SIS-578

 .../apache/sis/feature/ExpressionOperation.java    |  26 ++--
 .../org/apache/sis/feature/FeatureOperations.java  |   6 +-
 .../org/apache/sis/filter/ArithmeticFunction.java  |  30 ++--
 .../org/apache/sis/filter/AssociationValue.java    |  10 +-
 .../java/org/apache/sis/filter/BinaryFunction.java |  25 ++-
 .../apache/sis/filter/BinaryGeometryFilter.java    |  73 +++++----
 .../org/apache/sis/filter/BinarySpatialFilter.java |  16 +-
 .../org/apache/sis/filter/ComparisonFilter.java    |  83 +++++-----
 .../org/apache/sis/filter/ConvertFunction.java     |  10 +-
 .../apache/sis/filter/DefaultFilterFactory.java    | 168 ++++++++++-----------
 .../org/apache/sis/filter/DefaultSortProperty.java |   8 +-
 .../java/org/apache/sis/filter/DistanceFilter.java |  18 +--
 .../java/org/apache/sis/filter/Expression.java     |  11 +-
 .../main/java/org/apache/sis/filter/Filter.java    |  11 +-
 .../java/org/apache/sis/filter/FilterLiteral.java  |   5 +
 .../java/org/apache/sis/filter/FilterNode.java     |  86 -----------
 .../org/apache/sis/filter/IdentifierFilter.java    |  31 +++-
 .../java/org/apache/sis/filter/LeafExpression.java |  10 +-
 .../java/org/apache/sis/filter/LikeFilter.java     |  23 ++-
 .../java/org/apache/sis/filter/LogicalFilter.java  |  78 ++++++----
 .../java/org/apache/sis/filter/Optimization.java   |  99 ++++++------
 .../java/org/apache/sis/filter/PropertyValue.java  |  12 +-
 .../java/org/apache/sis/filter/TemporalFilter.java |  90 +++++------
 .../java/org/apache/sis/filter/UnaryFunction.java  |  28 ++--
 .../java/org/apache/sis/filter/package-info.java   |   2 +-
 .../java/org/apache/sis/image/ImageCombiner.java   |  11 +-
 .../java/org/apache/sis/image/ImageProcessor.java  |   6 +-
 .../sis/internal/coverage/j2d/ObservableImage.java |   5 +-
 .../sis/internal/filter/FunctionRegister.java      |   4 +-
 .../sis/internal/filter/GeometryConverter.java     |  18 ++-
 .../java/org/apache/sis/internal/filter/Node.java  |  24 ++-
 .../sis/internal/filter/SortByComparator.java      |   8 +-
 .../org/apache/sis/internal/filter/Visitor.java    |  21 +--
 .../internal/filter/sqlmm/FunctionWithSRID.java    |  15 +-
 .../internal/filter/sqlmm/GeometryConstructor.java |  18 ++-
 .../sis/internal/filter/sqlmm/GeometryParser.java  |   6 +-
 .../sis/internal/filter/sqlmm/OneGeometry.java     |  36 +++--
 .../apache/sis/internal/filter/sqlmm/Registry.java |   4 +-
 .../sis/internal/filter/sqlmm/ST_FromBinary.java   |   6 +-
 .../sis/internal/filter/sqlmm/ST_FromText.java     |   6 +-
 .../apache/sis/internal/filter/sqlmm/ST_Point.java |  22 ++-
 .../sis/internal/filter/sqlmm/ST_Transform.java    |  18 ++-
 .../sis/internal/filter/sqlmm/SpatialFunction.java |   2 +-
 .../sis/internal/filter/sqlmm/TwoGeometries.java   |  40 +++--
 .../sis/internal/filter/sqlmm/package-info.java    |   2 +-
 .../geoapi/filter/BetweenComparisonOperator.java   |   6 +-
 .../geoapi/filter/BinaryComparisonOperator.java    |   4 +-
 .../internal/geoapi/filter/FilterExpressions.java  |  15 +-
 .../apache/sis/internal/geoapi/filter/Literal.java |   2 +-
 .../internal/geoapi/filter/LogicalOperator.java    |   4 +-
 .../sis/internal/geoapi/filter/SortProperty.java   |   2 +-
 .../sis/filter/BinarySpatialFilterTestCase.java    |   7 +-
 .../apache/sis/filter/IdentifierFilterTest.java    |   4 +-
 .../org/apache/sis/filter/LeafExpressionTest.java  |   3 +-
 .../org/apache/sis/filter/LogicalFilterTest.java   |  27 +++-
 .../java/org/apache/sis/filter/PeriodLiteral.java  |   1 +
 .../org/apache/sis/filter/TemporalFilterTest.java  |   4 +-
 .../internal/filter/sqlmm/RegistryTestCase.java    |   4 +-
 .../sis/internal/sql/feature/FeatureStream.java    |   2 +-
 .../sql/feature/SelectionClauseWriter.java         |  19 +--
 .../sql/feature/SelectionClauseWriterTest.java     |   4 +-
 .../java/org/apache/sis/storage/FeatureQuery.java  |  24 +--
 .../java/org/apache/sis/storage/FeatureSubset.java |   4 +-
 .../sis/storage/aggregate/JoinFeatureSet.java      |   8 +-
 .../org/apache/sis/storage/FeatureQueryTest.java   |  24 +--
 65 files changed, 783 insertions(+), 616 deletions(-)

diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java
index 964a1c143b,efbc5cb83c..0d0531ee49
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/ExpressionOperation.java
@@@ -58,7 -64,7 +58,7 @@@ final class ExpressionOperation<V> exte
       * The expression on which to delegate the execution of this operation.
       */
      @SuppressWarnings("serial")                         // Not statically typed as serializable.
-     private final Function<? super AbstractFeature, ? extends V> expression;
 -    private final Function<Feature, ? extends V> expression;
++    private final Function<AbstractFeature, ? extends V> expression;
  
      /**
       * The type of result of evaluating the expression.
@@@ -81,14 -88,14 +81,14 @@@
       * @param result          type of values computed by the expression.
       */
      ExpressionOperation(final Map<String,?> identification,
-                         final Function<? super AbstractFeature, ? extends V> expression,
 -                        final Function<Feature, ? extends V> expression,
 -                        final AttributeType<? super V> result)
++                        final Function<AbstractFeature, ? extends V> expression,
 +                        final DefaultAttributeType<? super V> result)
      {
          super(identification);
          this.expression = expression;
          this.result     = result;
          if (expression instanceof Expression<?,?>) {
-             dependencies = DependencyFinder.search((Expression<Object,?>) expression);
 -            dependencies = DependencyFinder.search((Expression<Feature,?>) expression);
++            dependencies = DependencyFinder.search((Expression<AbstractFeature,?>) expression);
          } else {
              dependencies = Set.of();
          }
@@@ -157,10 -164,8 +157,8 @@@
      /**
       * An expression visitor for finding all dependencies of a given expression.
       * The dependencies are feature properties read by {@link ValueReference} nodes.
-      *
-      * @todo The first parameterized type should be {@code Feature} instead of {@code Object}.
       */
-     private static final class DependencyFinder extends Visitor<Object, Collection<String>> {
 -    private static final class DependencyFinder extends Visitor<Feature, Collection<String>> {
++    private static final class DependencyFinder extends Visitor<AbstractFeature, Collection<String>> {
          /**
           * The unique instance.
           */
@@@ -172,7 -177,7 +170,7 @@@
           * @param  expression  the expression for which to get dependencies.
           * @return all dependencies recognized by this method.
           */
-         static Set<String> search(final Expression<Object,?> expression) {
 -        static Set<String> search(final Expression<Feature,?> expression) {
++        static Set<String> search(final Expression<AbstractFeature,?> expression) {
              final Set<String> dependencies = new HashSet<>();
              VISITOR.visit(expression, dependencies);
              return Set.copyOf(dependencies);
@@@ -183,13 -188,13 +181,13 @@@
           */
          private DependencyFinder() {
              setLogicalHandlers((f, dependencies) -> {
-                 final var filter = (LogicalOperator<Object>) f;
-                 for (Filter<Object> child : filter.getOperands()) {
 -                final var filter = (LogicalOperator<Feature>) f;
 -                for (Filter<Feature> child : filter.getOperands()) {
++                final var filter = (LogicalOperator<AbstractFeature>) f;
++                for (Filter<AbstractFeature> child : filter.getOperands()) {
                      visit(child, dependencies);
                  }
              });
              setExpressionHandler(FunctionNames.ValueReference, (e, dependencies) -> {
-                 final var expression = (ValueReference<Object,?>) e;
 -                final var expression = (ValueReference<Feature,?>) e;
++                final var expression = (ValueReference<AbstractFeature,?>) e;
                  final String propName = expression.getXPath();
                  if (!propName.trim().isEmpty()) {
                      dependencies.add(propName);
@@@ -201,8 -206,8 +199,8 @@@
           * Fallback for all filters not explicitly handled by the setting applied in the constructor.
           */
          @Override
-         protected void typeNotFound(final Enum<?> type, final Filter<Object> filter, final Collection<String> dependencies) {
-             for (final Expression<Object,?> f : filter.getExpressions()) {
 -        protected void typeNotFound(final CodeList<?> type, final Filter<Feature> filter, final Collection<String> dependencies) {
 -            for (final Expression<Feature,?> f : filter.getExpressions()) {
++        protected void typeNotFound(final Enum<?> type, final Filter<AbstractFeature> filter, final Collection<String> dependencies) {
++            for (final Expression<AbstractFeature,?> f : filter.getExpressions()) {
                  visit(f, dependencies);
              }
          }
@@@ -211,8 -216,8 +209,8 @@@
           * Fallback for all expressions not explicitly handled by the setting applied in the constructor.
           */
          @Override
-         protected void typeNotFound(final String type, final Expression<Object,?> expression, final Collection<String> dependencies) {
-             for (final Expression<Object,?> p : expression.getParameters()) {
 -        protected void typeNotFound(final String type, final Expression<Feature,?> expression, final Collection<String> dependencies) {
 -            for (final Expression<Feature,?> p : expression.getParameters()) {
++        protected void typeNotFound(final String type, final Expression<AbstractFeature,?> expression, final Collection<String> dependencies) {
++            for (final Expression<AbstractFeature,?> p : expression.getParameters()) {
                  visit(p, dependencies);
              }
          }
diff --cc core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
index 3ddd0ff779,b4ab1692d0..98b451ddd7
--- a/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/feature/FeatureOperations.java
@@@ -286,9 -279,9 +286,9 @@@ public final class FeatureOperations ex
       *
       * @since 1.4
       */
 -    public static <V> Operation expression(final Map<String,?> identification,
 -                                           final Function<Feature, ? extends V> expression,
 -                                           final AttributeType<? super V> result)
 +    public static <V> AbstractOperation expression(final Map<String,?> identification,
-                                            final Function<? super AbstractFeature, ? extends V> expression,
++                                           final Function<AbstractFeature, ? extends V> expression,
 +                                           final DefaultAttributeType<? super V> result)
      {
          ArgumentChecks.ensureNonNull("expression", expression);
          return POOL.unique(new ExpressionOperation<>(identification, expression, result));
@@@ -310,9 -303,9 +310,9 @@@
       *
       * @since 1.4
       */
 -    public static <V> Operation expressionToResult(final Map<String,?> identification,
 -                                                   final Expression<Feature, ?> expression,
 -                                                   final AttributeType<V> result)
 +    public static <V> AbstractOperation expressionToResult(final Map<String,?> identification,
-                                                    final Expression<? super AbstractFeature, ?> expression,
++                                                   final Expression<AbstractFeature, ?> expression,
 +                                                   final DefaultAttributeType<V> result)
      {
          return expression(identification, expression.toValueType(result.getValueClass()), result);
      }
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/ArithmeticFunction.java
index c418363a95,0cd6fa5a71..66fff6b926
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/ArithmeticFunction.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/ArithmeticFunction.java
@@@ -38,9 -39,9 +38,9 @@@ import org.apache.sis.feature.DefaultAt
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   *
   * @since 1.1
   */
@@@ -141,12 -142,12 +141,12 @@@ abstract class ArithmeticFunction<R> ex
          private static final long serialVersionUID = 5445433312445869201L;
  
          /** Description of results of the {@code "Add"} expression. */
 -        private static final AttributeType<Number> TYPE = createNumericType(FunctionNames.Add);
 -        @Override protected AttributeType<Number> expectedType() {return TYPE;}
 +        private static final DefaultAttributeType<Number> TYPE = createNumericType(FunctionNames.Add);
 +        @Override protected DefaultAttributeType<Number> expectedType() {return TYPE;}
  
          /** Creates a new expression for the {@code "Add"} operation. */
-         Add(final Expression<? super R, ? extends Number> expression1,
-             final Expression<? super R, ? extends Number> expression2)
+         Add(final Expression<R, ? extends Number> expression1,
+             final Expression<R, ? extends Number> expression2)
          {
              super(expression1, expression2);
          }
@@@ -181,12 -182,12 +181,12 @@@
          private static final long serialVersionUID = 3048878022726271508L;
  
          /** Description of results of the {@code "Subtract"} expression. */
 -        private static final AttributeType<Number> TYPE = createNumericType(FunctionNames.Subtract);
 -        @Override protected AttributeType<Number> expectedType() {return TYPE;}
 +        private static final DefaultAttributeType<Number> TYPE = createNumericType(FunctionNames.Subtract);
 +        @Override protected DefaultAttributeType<Number> expectedType() {return TYPE;}
  
          /** Creates a new expression for the {@code "Subtract"} operation. */
-         Subtract(final Expression<? super R, ? extends Number> expression1,
-                  final Expression<? super R, ? extends Number> expression2)
+         Subtract(final Expression<R, ? extends Number> expression1,
+                  final Expression<R, ? extends Number> expression2)
          {
              super(expression1, expression2);
          }
@@@ -221,12 -222,12 +221,12 @@@
          private static final long serialVersionUID = -1300022614832645625L;
  
          /** Description of results of the {@code "Multiply"} expression. */
 -        private static final AttributeType<Number> TYPE = createNumericType(FunctionNames.Multiply);
 -        @Override protected AttributeType<Number> expectedType() {return TYPE;}
 +        private static final DefaultAttributeType<Number> TYPE = createNumericType(FunctionNames.Multiply);
 +        @Override protected DefaultAttributeType<Number> expectedType() {return TYPE;}
  
          /** Creates a new expression for the {@code "Multiply"} operation. */
-         Multiply(final Expression<? super R, ? extends Number> expression1,
-                  final Expression<? super R, ? extends Number> expression2)
+         Multiply(final Expression<R, ? extends Number> expression1,
+                  final Expression<R, ? extends Number> expression2)
          {
              super(expression1, expression2);
          }
@@@ -261,12 -262,12 +261,12 @@@
          private static final long serialVersionUID = -7709291845568648891L;
  
          /** Description of results of the {@code "Divide"} expression. */
 -        private static final AttributeType<Number> TYPE = createNumericType(FunctionNames.Divide);
 -        @Override protected AttributeType<Number> expectedType() {return TYPE;}
 +        private static final DefaultAttributeType<Number> TYPE = createNumericType(FunctionNames.Divide);
 +        @Override protected DefaultAttributeType<Number> expectedType() {return TYPE;}
  
          /** Creates a new expression for the {@code "Divide"} operation. */
-         Divide(final Expression<? super R, ? extends Number> expression1,
-                final Expression<? super R, ? extends Number> expression2)
+         Divide(final Expression<R, ? extends Number> expression1,
+                final Expression<R, ? extends Number> expression2)
          {
              super(expression1, expression2);
          }
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/AssociationValue.java
index e29b4c4be3,c8d714b8d5..a3f20b3055
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/AssociationValue.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/AssociationValue.java
@@@ -94,11 -94,14 +94,19 @@@ final class AssociationValue<V> extend
          this.accessor = accessor;
      }
  
 +    @Override
 +    public final ScopedName getFunctionName() {
 +        return Name.VALUE_REFERENCE;
 +    }
 +
+     /**
+      * Returns the class of resources expected by this expression.
+      */
+     @Override
 -    public final Class<Feature> getResourceClass() {
 -        return Feature.class;
++    public final Class<AbstractFeature> getResourceClass() {
++        return AbstractFeature.class;
+     }
+ 
      /**
       * For {@link #toString()} implementation.
       */
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/BinaryFunction.java
index 8a542e5f17,a149e05cf9..ea158b7b0e
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/BinaryFunction.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/BinaryFunction.java
@@@ -35,9 -39,9 +35,9 @@@ import org.apache.sis.internal.filter.N
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <R>   the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>   the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <V1>  the type of value computed by the first expression.
   * @param  <V2>  the type of value computed by the second expression.
   *
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/BinaryGeometryFilter.java
index b0be4785f0,5bb4d09ba6..457d8ea980
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/BinaryGeometryFilter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/BinaryGeometryFilter.java
@@@ -42,14 -49,14 +43,14 @@@ import org.apache.sis.internal.geoapi.f
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
   * @author  Alexis Manin (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <G>  the implementation type of geometry objects.
   *
   * @since 1.1
   */
- abstract class BinaryGeometryFilter<R,G> extends FilterNode<R> implements Optimization.OnFilter<R> {
 -abstract class BinaryGeometryFilter<R,G> extends Node implements SpatialOperator<R>, Optimization.OnFilter<R> {
++abstract class BinaryGeometryFilter<R,G> extends Node implements Optimization.OnFilter<R> {
      /**
       * For cross-version compatibility.
       */
@@@ -57,15 -64,19 +58,15 @@@
  
      /**
       * The first of the two expressions to be used by this function.
 -     *
 -     * @see BinarySpatialOperator#getOperand1()
       */
      @SuppressWarnings("serial")         // Most SIS implementations are serializable.
-     protected final Expression<? super R, GeometryWrapper<G>> expression1;
+     protected final Expression<R, GeometryWrapper<G>> expression1;
  
      /**
       * The second of the two expressions to be used by this function.
 -     *
 -     * @see BinarySpatialOperator#getOperand2()
       */
      @SuppressWarnings("serial")         // Most SIS implementations are serializable.
-     protected final Expression<? super R, GeometryWrapper<G>> expression2;
+     protected final Expression<R, GeometryWrapper<G>> expression2;
  
      /**
       * The preferred CRS and other context to use if geometry transformations are needed.
@@@ -211,7 -232,7 +222,7 @@@
                      final GeometryWrapper<G> geometry    = wrapper.apply(null);
                      final GeometryWrapper<G> transformed = geometry.transform(targetCRS);
                      if (geometry != transformed) {
-                         literal = (Literal<? super R, ?>) Optimization.literal(transformed);
 -                        literal = Optimization.literal(transformed);
++                        literal = (Literal<R,?>) Optimization.literal(transformed);
                          if (literal == effective1) effective1 = literal;
                          else effective2 = literal;
                      }
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/BinarySpatialFilter.java
index a8e069b873,c9d7795038..e3ee507b62
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/BinarySpatialFilter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/BinarySpatialFilter.java
@@@ -36,9 -38,9 +36,9 @@@ import org.apache.sis.internal.geoapi.f
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <G>  the implementation type of geometry objects.
   *
   * @since 1.1
@@@ -112,14 -114,16 +112,14 @@@ final class BinarySpatialFilter<R,G> ex
      /**
       * Returns the first expression to be evaluated.
       */
-     public Expression<? super R, ?> getOperand1() {
 -    @Override
+     public Expression<R,?> getOperand1() {
          return original(expression1);
      }
  
      /**
       * Returns the second expression to be evaluated.
       */
-     public Expression<? super R, ?> getOperand2() {
 -    @Override
+     public Expression<R,?> getOperand2() {
          return original(expression2);
      }
  
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/ComparisonFilter.java
index 7934aaf5b9,b593f590c3..a3e29391a9
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/ComparisonFilter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/ComparisonFilter.java
@@@ -36,12 -36,15 +36,13 @@@ import java.time.temporal.ChronoField
  import java.time.temporal.Temporal;
  import org.apache.sis.math.Fraction;
  import org.apache.sis.util.ArgumentChecks;
+ import org.apache.sis.internal.filter.Node;
  
  // Branch-dependent imports
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.MatchAction;
 -import org.opengis.filter.ComparisonOperatorName;
 -import org.opengis.filter.BinaryComparisonOperator;
 -import org.opengis.filter.BetweenComparisonOperator;
 +import org.apache.sis.internal.geoapi.filter.MatchAction;
 +import org.apache.sis.internal.geoapi.filter.ComparisonOperatorName;
 +import org.apache.sis.internal.geoapi.filter.BinaryComparisonOperator;
 +import org.apache.sis.internal.geoapi.filter.BetweenComparisonOperator;
  
  
  /**
@@@ -65,9 -68,9 +66,9 @@@
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   *
   * @since 1.1
   */
@@@ -810,10 -813,20 +811,24 @@@ abstract class ComparisonFilter<R> exte
              this.upper = new    LessThanOrEqualTo<>(expression, upper, true, MatchAction.ANY);
          }
  
 +        @Override public ComparisonOperatorName getOperatorType() {
 +            return ComparisonOperatorName.PROPERTY_IS_BETWEEN;
 +        }
 +
+         /**
+          * Creates a new filter of the same type but different parameters.
+          */
+         @Override
+         public Filter<R> recreate(final Expression<R,?>[] effective) {
+             return new Between<>(effective[0], effective[1], effective[2]);
+         }
+ 
+         /** Returns the class of resources expected by this filter. */
+         @Override public final Class<? super R> getResourceClass() {
+             return specializedClass(lower.getResourceClass(),
+                                     upper.getResourceClass());
+         }
+ 
          /**
           * Returns the 3 children of this node. Since {@code lower.expression2}
           * is the same as {@code upper.expression1}, that repetition is omitted.
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/ConvertFunction.java
index b163cadbfe,3095f4cb58..f58012ef8f
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/ConvertFunction.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/ConvertFunction.java
@@@ -36,9 -37,9 +36,9 @@@ import org.apache.sis.feature.DefaultFe
   * Expression whose results are converted to a different type.
   *
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.2
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <S>  the type of value computed by the wrapped exception. This is the type to convert.
   * @param  <V>  the type of value computed by this expression. This is the type after conversion.
   *
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
index f4f35e0cc4,7d56e36cc1..5bcf267097
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/DefaultFilterFactory.java
@@@ -55,9 -48,9 +55,9 @@@ import org.apache.sis.internal.geoapi.f
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.2
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) to use as inputs.
 + * @param  <R>  the type of resources (e.g. {@link AbstractFeature}) to use as inputs.
   * @param  <G>  base class of geometry objects. The implementation-neutral type is GeoAPI {@link Geometry},
   *              but this factory allows the use of other implementations such as JTS
   *              {@link org.locationtech.jts.geom.Geometry} or ESRI {@link com.esri.core.geometry.Geometry}.
@@@ -179,8 -185,29 +179,8 @@@ public abstract class DefaultFilterFact
           * @return the predicate.
           */
          @Override
 -        public ResourceId<Feature> resourceId(final String identifier) {
 -            return new IdentifierFilter(identifier);
 -        }
 -
 -        /**
 -         * Creates a new predicate to identify an identifiable resource within a filter expression.
 -         * If {@code startTime} and {@code endTime} are non-null, the filter will select all versions
 -         * of a resource between the specified dates.
 -         *
 -         * @param  identifier  identifier of the resource that shall be selected by the predicate.
 -         * @param  version     version of the resource to select, or {@code null} for any version.
 -         * @param  startTime   start time of the resource to select, or {@code null} if none.
 -         * @param  endTime     end time of the resource to select, or {@code null} if none.
 -         * @return the predicate.
 -         *
 -         * @todo Current implementation ignores the version, start time and end time.
 -         *       This limitation may be resolved in a future version.
 -         */
 -        @Override
 -        public ResourceId<Feature> resourceId(final String identifier, final Version version,
 -                                              final Instant startTime, final Instant endTime)
 -        {
 +        public Filter<AbstractFeature> resourceId(final String identifier) {
-             return new IdentifierFilter<>(identifier);
+             return new IdentifierFilter(identifier);
          }
  
          /**
@@@ -257,12 -250,19 +257,12 @@@
       *
       * @param  expression1     the first of the two expressions to be used by this comparator.
       * @param  expression2     the second of the two expressions to be used by this comparator.
 -     * @param  isMatchingCase  specifies whether comparisons are case sensitive.
 -     * @param  matchAction     specifies how the comparisons shall be evaluated for a collection of values.
       * @return a filter evaluating {@code expression1} = {@code expression2}.
 -     *
 -     * @see ComparisonOperatorName#PROPERTY_IS_EQUAL_TO
 -     * @todo Revisit if we can be more specific on the second parameterized type in expressions.
       */
-     public Filter<R> equal(final Expression<? super R, ?> expression1,
-                            final Expression<? super R, ?> expression2)
 -    @Override
 -    public BinaryComparisonOperator<R> equal(final Expression<R,?> expression1,
 -                                             final Expression<R,?> expression2,
 -                                             boolean isMatchingCase, MatchAction matchAction)
++    public Filter<R> equal(final Expression<R,?> expression1,
++                           final Expression<R,?> expression2)
      {
 -        return new ComparisonFilter.EqualTo<>(expression1, expression2, isMatchingCase, matchAction);
 +        return new ComparisonFilter.EqualTo<>(expression1, expression2, true, MatchAction.ANY);
      }
  
      /**
@@@ -270,12 -270,19 +270,12 @@@
       *
       * @param  expression1     the first of the two expressions to be used by this comparator.
       * @param  expression2     the second of the two expressions to be used by this comparator.
 -     * @param  isMatchingCase  specifies whether comparisons are case sensitive.
 -     * @param  matchAction     specifies how the comparisons shall be evaluated for a collection of values.
       * @return a filter evaluating {@code expression1} ≠ {@code expression2}.
 -     *
 -     * @see ComparisonOperatorName#PROPERTY_IS_NOT_EQUAL_TO
 -     * @todo Revisit if we can be more specific on the second parameterized type in expressions.
       */
-     public Filter<R> notEqual(final Expression<? super R, ?> expression1,
-                               final Expression<? super R, ?> expression2)
 -    @Override
 -    public BinaryComparisonOperator<R> notEqual(final Expression<R,?> expression1,
 -                                                final Expression<R,?> expression2,
 -                                                boolean isMatchingCase, MatchAction matchAction)
++    public Filter<R> notEqual(final Expression<R,?> expression1,
++                              final Expression<R,?> expression2)
      {
 -        return new ComparisonFilter.NotEqualTo<>(expression1, expression2, isMatchingCase, matchAction);
 +        return new ComparisonFilter.NotEqualTo<>(expression1, expression2, true, MatchAction.ANY);
      }
  
      /**
@@@ -283,12 -290,19 +283,12 @@@
       *
       * @param  expression1     the first of the two expressions to be used by this comparator.
       * @param  expression2     the second of the two expressions to be used by this comparator.
 -     * @param  isMatchingCase  specifies whether comparisons are case sensitive.
 -     * @param  matchAction     specifies how the comparisons shall be evaluated for a collection of values.
       * @return a filter evaluating {@code expression1} &lt; {@code expression2}.
 -     *
 -     * @see ComparisonOperatorName#PROPERTY_IS_LESS_THAN
 -     * @todo Revisit if we can be more specific on the second parameterized type in expressions.
       */
-     public Filter<R> less(final Expression<? super R, ?> expression1,
-                           final Expression<? super R, ?> expression2)
 -    @Override
 -    public BinaryComparisonOperator<R> less(final Expression<R,?> expression1,
 -                                            final Expression<R,?> expression2,
 -                                            boolean isMatchingCase, MatchAction matchAction)
++    public Filter<R> less(final Expression<R,?> expression1,
++                          final Expression<R,?> expression2)
      {
 -        return new ComparisonFilter.LessThan<>(expression1, expression2, isMatchingCase, matchAction);
 +        return new ComparisonFilter.LessThan<>(expression1, expression2, true, MatchAction.ANY);
      }
  
      /**
@@@ -296,12 -310,19 +296,12 @@@
       *
       * @param  expression1     the first of the two expressions to be used by this comparator.
       * @param  expression2     the second of the two expressions to be used by this comparator.
 -     * @param  isMatchingCase  specifies whether comparisons are case sensitive.
 -     * @param  matchAction     specifies how the comparisons shall be evaluated for a collection of values.
       * @return a filter evaluating {@code expression1} &gt; {@code expression2}.
 -     *
 -     * @see ComparisonOperatorName#PROPERTY_IS_GREATER_THAN
 -     * @todo Revisit if we can be more specific on the second parameterized type in expressions.
       */
-     public Filter<R> greater(final Expression<? super R, ?> expression1,
-                              final Expression<? super R, ?> expression2)
 -    @Override
 -    public BinaryComparisonOperator<R> greater(final Expression<R,?> expression1,
 -                                               final Expression<R,?> expression2,
 -                                               boolean isMatchingCase, MatchAction matchAction)
++    public Filter<R> greater(final Expression<R,?> expression1,
++                             final Expression<R,?> expression2)
      {
 -        return new ComparisonFilter.GreaterThan<>(expression1, expression2, isMatchingCase, matchAction);
 +        return new ComparisonFilter.GreaterThan<>(expression1, expression2, true, MatchAction.ANY);
      }
  
      /**
@@@ -309,12 -330,19 +309,12 @@@
       *
       * @param  expression1     the first of the two expressions to be used by this comparator.
       * @param  expression2     the second of the two expressions to be used by this comparator.
 -     * @param  isMatchingCase  specifies whether comparisons are case sensitive.
 -     * @param  matchAction     specifies how the comparisons shall be evaluated for a collection of values.
       * @return a filter evaluating {@code expression1} ≤ {@code expression2}.
 -     *
 -     * @see ComparisonOperatorName#PROPERTY_IS_LESS_THAN_OR_EQUAL_TO
 -     * @todo Revisit if we can be more specific on the second parameterized type in expressions.
       */
-     public Filter<R> lessOrEqual(final Expression<? super R, ?> expression1,
-                                  final Expression<? super R, ?> expression2)
 -    @Override
 -    public BinaryComparisonOperator<R> lessOrEqual(final Expression<R,?> expression1,
 -                                                   final Expression<R,?> expression2,
 -                                                   boolean isMatchingCase, MatchAction matchAction)
++    public Filter<R> lessOrEqual(final Expression<R,?> expression1,
++                                 final Expression<R,?> expression2)
      {
 -        return new ComparisonFilter.LessThanOrEqualTo<>(expression1, expression2, isMatchingCase, matchAction);
 +        return new ComparisonFilter.LessThanOrEqualTo<>(expression1, expression2, true, MatchAction.ANY);
      }
  
      /**
@@@ -322,12 -350,19 +322,12 @@@
       *
       * @param  expression1     the first of the two expressions to be used by this comparator.
       * @param  expression2     the second of the two expressions to be used by this comparator.
 -     * @param  isMatchingCase  specifies whether comparisons are case sensitive.
 -     * @param  matchAction     specifies how the comparisons shall be evaluated for a collection of values.
       * @return a filter evaluating {@code expression1} ≥ {@code expression2}.
 -     *
 -     * @see ComparisonOperatorName#PROPERTY_IS_GREATER_THAN_OR_EQUAL_TO
 -     * @todo Revisit if we can be more specific on the second parameterized type in expressions.
       */
-     public Filter<R> greaterOrEqual(final Expression<? super R, ?> expression1,
-                                     final Expression<? super R, ?> expression2)
 -    @Override
 -    public BinaryComparisonOperator<R> greaterOrEqual(final Expression<R,?> expression1,
 -                                                      final Expression<R,?> expression2,
 -                                                      boolean isMatchingCase, MatchAction matchAction)
++    public Filter<R> greaterOrEqual(final Expression<R,?> expression1,
++                                    final Expression<R,?> expression2)
      {
 -        return new ComparisonFilter.GreaterThanOrEqualTo<>(expression1, expression2, isMatchingCase, matchAction);
 +        return new ComparisonFilter.GreaterThanOrEqualTo<>(expression1, expression2, true, MatchAction.ANY);
      }
  
      /**
@@@ -340,26 -375,14 +340,26 @@@
       * @return a filter evaluating ({@code expression} ≥ {@code lowerBoundary})
       *                       &amp; ({@code expression} ≤ {@code upperBoundary}).
       */
-     public Filter<R> between(final Expression<? super R, ?> expression,
-                              final Expression<? super R, ?> lowerBoundary,
-                              final Expression<? super R, ?> upperBoundary)
 -    @Override
 -    public BetweenComparisonOperator<R> between(final Expression<R,?> expression,
 -                                                final Expression<R,?> lowerBoundary,
 -                                                final Expression<R,?> upperBoundary)
++    public Filter<R> between(final Expression<R,?> expression,
++                             final Expression<R,?> lowerBoundary,
++                             final Expression<R,?> upperBoundary)
      {
          return new ComparisonFilter.Between<>(expression, lowerBoundary, upperBoundary);
      }
  
 +    /**
 +     * Character string comparison operator with pattern matching and default wildcards.
 +     * The wildcard character is {@code '%'}, the single character is {@code '_'} and
 +     * the escape character is {@code '\\'}. The comparison is case-sensitive.
 +     *
 +     * @param  expression  source of values to compare against the pattern.
 +     * @param  pattern     pattern to match against expression values.
 +     * @return a character string comparison operator with pattern matching.
 +     */
-     public Filter<R> like(Expression<? super R, ?> expression, String pattern) {
++    public Filter<R> like(Expression<R,?> expression, String pattern) {
 +        return like(expression, pattern, '%', '_', '\\', true);
 +    }
 +
      /**
       * Character string comparison operator with pattern matching and specified wildcards.
       *
@@@ -371,7 -394,8 +371,7 @@@
       * @param  isMatchingCase  specifies how a filter expression processor should perform string comparisons.
       * @return a character string comparison operator with pattern matching.
       */
-     public Filter<R> like(final Expression<? super R, ?> expression, final String pattern,
 -    @Override
 -    public LikeOperator<R> like(final Expression<R,?> expression, final String pattern,
++    public Filter<R> like(final Expression<R,?> expression, final String pattern,
              final char wildcard, final char singleChar, final char escape, final boolean isMatchingCase)
      {
          return new LikeFilter<>(expression, pattern, wildcard, singleChar, escape, isMatchingCase);
@@@ -384,7 -408,8 +384,7 @@@
       * @param  expression  source of values to compare against {@code null}.
       * @return a filter that checks if an expression's value is {@code null}.
       */
-     public Filter<R> isNull(final Expression<? super R, ?> expression) {
 -    @Override
 -    public NullOperator<R> isNull(final Expression<R,?> expression) {
++    public Filter<R> isNull(final Expression<R,?> expression) {
          return new UnaryFunction.IsNull<>(expression);
      }
  
@@@ -412,7 -437,8 +412,7 @@@
       * @see org.apache.sis.xml.NilObject
       * @see org.apache.sis.xml.NilReason
       */
-     public Filter<R> isNil(final Expression<? super R, ?> expression, final String nilReason) {
 -    @Override
 -    public NilOperator<R> isNil(final Expression<R,?> expression, final String nilReason) {
++    public Filter<R> isNil(final Expression<R,?> expression, final String nilReason) {
          return new UnaryFunction.IsNil<>(expression, nilReason);
      }
  
@@@ -422,8 -448,11 +422,8 @@@
       * @param  operand1  the first operand of the AND operation.
       * @param  operand2  the second operand of the AND operation.
       * @return a filter evaluating {@code operand1 AND operand2}.
 -     *
 -     * @see LogicalOperatorName#AND
       */
-     public Filter<R> and(final Filter<? super R> operand1, final Filter<? super R> operand2) {
 -    @Override
 -    public LogicalOperator<R> and(final Filter<R> operand1, final Filter<R> operand2) {
++    public Filter<R> and(final Filter<R> operand1, final Filter<R> operand2) {
          ArgumentChecks.ensureNonNull("operand1", operand1);
          ArgumentChecks.ensureNonNull("operand2", operand2);
          return new LogicalFilter.And<>(operand1, operand2);
@@@ -435,8 -464,11 +435,8 @@@
       * @param  operands  a collection of at least 2 operands.
       * @return a filter evaluating {@code operand1 AND operand2 AND operand3}…
       * @throws IllegalArgumentException if the given collection contains less than 2 elements.
 -     *
 -     * @see LogicalOperatorName#AND
       */
-     public Filter<R> and(final Collection<? extends Filter<? super R>> operands) {
 -    @Override
 -    public LogicalOperator<R> and(final Collection<? extends Filter<R>> operands) {
++    public Filter<R> and(final Collection<? extends Filter<R>> operands) {
          return new LogicalFilter.And<>(operands);
      }
  
@@@ -446,8 -478,11 +446,8 @@@
       * @param  operand1  the first operand of the OR operation.
       * @param  operand2  the second operand of the OR operation.
       * @return a filter evaluating {@code operand1 OR operand2}.
 -     *
 -     * @see LogicalOperatorName#OR
       */
-     public Filter<R> or(final Filter<? super R> operand1, final Filter<? super R> operand2) {
 -    @Override
 -    public LogicalOperator<R> or(final Filter<R> operand1, final Filter<R> operand2) {
++    public Filter<R> or(final Filter<R> operand1, final Filter<R> operand2) {
          ArgumentChecks.ensureNonNull("operand1", operand1);
          ArgumentChecks.ensureNonNull("operand2", operand2);
          return new LogicalFilter.Or<>(operand1, operand2);
@@@ -459,8 -494,11 +459,8 @@@
       * @param  operands  a collection of at least 2 operands.
       * @return a filter evaluating {@code operand1 OR operand2 OR operand3}…
       * @throws IllegalArgumentException if the given collection contains less than 2 elements.
 -     *
 -     * @see LogicalOperatorName#OR
       */
-     public Filter<R> or(final Collection<? extends Filter<? super R>> operands) {
 -    @Override
 -    public LogicalOperator<R> or(final Collection<? extends Filter<R>> operands) {
++    public Filter<R> or(final Collection<? extends Filter<R>> operands) {
          return new LogicalFilter.Or<>(operands);
      }
  
@@@ -469,8 -507,11 +469,8 @@@
       *
       * @param  operand  the operand of the NOT operation.
       * @return a filter evaluating {@code NOT operand}.
 -     *
 -     * @see LogicalOperatorName#NOT
       */
-     public Filter<R> not(final Filter<? super R> operand) {
 -    @Override
 -    public LogicalOperator<R> not(final Filter<R> operand) {
++    public Filter<R> not(final Filter<R> operand) {
          return new LogicalFilter.Not<>(operand);
      }
  
@@@ -481,8 -522,13 +481,8 @@@
       * @param  geometry  expression fetching the geometry to check for interaction with bounds.
       * @param  bounds    the bounds to check geometry against.
       * @return a filter checking for any interactions between the bounding boxes.
 -     *
 -     * @see SpatialOperatorName#BBOX
 -     *
 -     * @todo Maybe the expression parameterized type should extend {@link Geometry}.
       */
-     public Filter<R> bbox(final Expression<? super R, ? extends G> geometry, final Envelope bounds) {
 -    @Override
 -    public BinarySpatialOperator<R> bbox(final Expression<R, ? extends G> geometry, final Envelope bounds) {
++    public Filter<R> bbox(final Expression<R, ? extends G> geometry, final Envelope bounds) {
          return new BinarySpatialFilter<>(library, geometry, bounds, wraparound);
      }
  
@@@ -492,9 -538,12 +492,9 @@@
       * @param  geometry1  expression fetching the first geometry of the binary operator.
       * @param  geometry2  expression fetching the second geometry of the binary operator.
       * @return a filter for the "Equals" operation between the two geometries.
 -     *
 -     * @see SpatialOperatorName#EQUALS
       */
-     public Filter<R> equals(final Expression<? super R, ? extends G> geometry1,
-                             final Expression<? super R, ? extends G> geometry2)
 -    @Override
 -    public BinarySpatialOperator<R> equals(final Expression<R, ? extends G> geometry1,
 -                                           final Expression<R, ? extends G> geometry2)
++    public Filter<R> equals(final Expression<R, ? extends G> geometry1,
++                            final Expression<R, ? extends G> geometry2)
      {
          return new BinarySpatialFilter<>(SpatialOperatorName.EQUALS, library, geometry1, geometry2);
      }
@@@ -505,9 -554,12 +505,9 @@@
       * @param  geometry1  expression fetching the first geometry of the binary operator.
       * @param  geometry2  expression fetching the second geometry of the binary operator.
       * @return a filter for the "Disjoint" operation between the two geometries.
 -     *
 -     * @see SpatialOperatorName#DISJOINT
       */
-     public Filter<R> disjoint(final Expression<? super R, ? extends G> geometry1,
-                               final Expression<? super R, ? extends G> geometry2)
 -    @Override
 -    public BinarySpatialOperator<R> disjoint(final Expression<R, ? extends G> geometry1,
 -                                             final Expression<R, ? extends G> geometry2)
++    public Filter<R> disjoint(final Expression<R, ? extends G> geometry1,
++                              final Expression<R, ? extends G> geometry2)
      {
          return new BinarySpatialFilter<>(SpatialOperatorName.DISJOINT, library, geometry1, geometry2);
      }
@@@ -518,9 -570,12 +518,9 @@@
       * @param  geometry1  expression fetching the first geometry of the binary operator.
       * @param  geometry2  expression fetching the second geometry of the binary operator.
       * @return a filter for the "Intersects" operation between the two geometries.
 -     *
 -     * @see SpatialOperatorName#INTERSECTS
       */
-     public Filter<R> intersects(final Expression<? super R, ? extends G> geometry1,
-                                 final Expression<? super R, ? extends G> geometry2)
 -    @Override
 -    public BinarySpatialOperator<R> intersects(final Expression<R, ? extends G> geometry1,
 -                                               final Expression<R, ? extends G> geometry2)
++    public Filter<R> intersects(final Expression<R, ? extends G> geometry1,
++                                final Expression<R, ? extends G> geometry2)
      {
          return new BinarySpatialFilter<>(SpatialOperatorName.INTERSECTS, library, geometry1, geometry2);
      }
@@@ -531,9 -586,12 +531,9 @@@
       * @param  geometry1  expression fetching the first geometry of the binary operator.
       * @param  geometry2  expression fetching the second geometry of the binary operator.
       * @return a filter for the "Touches" operation between the two geometries.
 -     *
 -     * @see SpatialOperatorName#TOUCHES
       */
-     public Filter<R> touches(final Expression<? super R, ? extends G> geometry1,
-                              final Expression<? super R, ? extends G> geometry2)
 -    @Override
 -    public BinarySpatialOperator<R> touches(final Expression<R, ? extends G> geometry1,
 -                                            final Expression<R, ? extends G> geometry2)
++    public Filter<R> touches(final Expression<R, ? extends G> geometry1,
++                             final Expression<R, ? extends G> geometry2)
      {
          return new BinarySpatialFilter<>(SpatialOperatorName.TOUCHES, library, geometry1, geometry2);
      }
@@@ -544,9 -602,12 +544,9 @@@
       * @param  geometry1  expression fetching the first geometry of the binary operator.
       * @param  geometry2  expression fetching the second geometry of the binary operator.
       * @return a filter for the "Crosses" operation between the two geometries.
 -     *
 -     * @see SpatialOperatorName#CROSSES
       */
-     public Filter<R> crosses(final Expression<? super R, ? extends G> geometry1,
-                              final Expression<? super R, ? extends G> geometry2)
 -    @Override
 -    public BinarySpatialOperator<R> crosses(final Expression<R, ? extends G> geometry1,
 -                                            final Expression<R, ? extends G> geometry2)
++    public Filter<R> crosses(final Expression<R, ? extends G> geometry1,
++                             final Expression<R, ? extends G> geometry2)
      {
          return new BinarySpatialFilter<>(SpatialOperatorName.CROSSES, library, geometry1, geometry2);
      }
@@@ -558,9 -619,12 +558,9 @@@
       * @param  geometry1  expression fetching the first geometry of the binary operator.
       * @param  geometry2  expression fetching the second geometry of the binary operator.
       * @return a filter for the "Within" operation between the two geometries.
 -     *
 -     * @see SpatialOperatorName#WITHIN
       */
-     public Filter<R> within(final Expression<? super R, ? extends G> geometry1,
-                             final Expression<? super R, ? extends G> geometry2)
 -    @Override
 -    public BinarySpatialOperator<R> within(final Expression<R, ? extends G> geometry1,
 -                                           final Expression<R, ? extends G> geometry2)
++    public Filter<R> within(final Expression<R, ? extends G> geometry1,
++                            final Expression<R, ? extends G> geometry2)
      {
          return new BinarySpatialFilter<>(SpatialOperatorName.WITHIN, library, geometry1, geometry2);
      }
@@@ -571,9 -635,12 +571,9 @@@
       * @param  geometry1  expression fetching the first geometry of the binary operator.
       * @param  geometry2  expression fetching the second geometry of the binary operator.
       * @return a filter for the "Contains" operation between the two geometries.
 -     *
 -     * @see SpatialOperatorName#CONTAINS
       */
-     public Filter<R> contains(final Expression<? super R, ? extends G> geometry1,
-                               final Expression<? super R, ? extends G> geometry2)
 -    @Override
 -    public BinarySpatialOperator<R> contains(final Expression<R, ? extends G> geometry1,
 -                                             final Expression<R, ? extends G> geometry2)
++    public Filter<R> contains(final Expression<R, ? extends G> geometry1,
++                              final Expression<R, ? extends G> geometry2)
      {
          return new BinarySpatialFilter<>(SpatialOperatorName.CONTAINS, library, geometry1, geometry2);
      }
@@@ -585,9 -652,12 +585,9 @@@
       * @param  geometry1  expression fetching the first geometry of the binary operator.
       * @param  geometry2  expression fetching the second geometry of the binary operator.
       * @return a filter for the "Overlaps" operation between the two geometries.
 -     *
 -     * @see SpatialOperatorName#OVERLAPS
       */
-     public Filter<R> overlaps(final Expression<? super R, ? extends G> geometry1,
-                               final Expression<? super R, ? extends G> geometry2)
 -    @Override
 -    public BinarySpatialOperator<R> overlaps(final Expression<R, ? extends G> geometry1,
 -                                             final Expression<R, ? extends G> geometry2)
++    public Filter<R> overlaps(final Expression<R, ? extends G> geometry1,
++                              final Expression<R, ? extends G> geometry2)
      {
          return new BinarySpatialFilter<>(SpatialOperatorName.OVERLAPS, library, geometry1, geometry2);
      }
@@@ -601,10 -671,13 +601,10 @@@
       * @param  distance   minimal distance for evaluating the expression as {@code true}.
       * @return operator that evaluates to {@code true} when all of a feature's geometry
       *         is more distant than the given distance from the second geometry.
 -     *
 -     * @see DistanceOperatorName#BEYOND
       */
-     public Filter<R> beyond(final Expression<? super R, ? extends G> geometry1,
-                             final Expression<? super R, ? extends G> geometry2,
 -    @Override
 -    public DistanceOperator<R> beyond(final Expression<R, ? extends G> geometry1,
 -                                      final Expression<R, ? extends G> geometry2,
 -                                      final Quantity<Length> distance)
++    public Filter<R> beyond(final Expression<R, ? extends G> geometry1,
++                            final Expression<R, ? extends G> geometry2,
 +                            final Quantity<Length> distance)
      {
          return new DistanceFilter<>(DistanceOperatorName.BEYOND, library, geometry1, geometry2, distance);
      }
@@@ -618,10 -691,13 +618,10 @@@
       * @param  distance   maximal distance for evaluating the expression as {@code true}.
       * @return operator that evaluates to {@code true} when any part of the feature's geometry
       *         lies within the given distance of the second geometry.
 -     *
 -     * @see DistanceOperatorName#WITHIN
       */
-     public Filter<R> within(final Expression<? super R, ? extends G> geometry1,
-                             final Expression<? super R, ? extends G> geometry2,
 -    @Override
 -    public DistanceOperator<R> within(final Expression<R, ? extends G> geometry1,
 -                                      final Expression<R, ? extends G> geometry2,
 -                                      final Quantity<Length> distance)
++    public Filter<R> within(final Expression<R, ? extends G> geometry1,
++                            final Expression<R, ? extends G> geometry2,
 +                            final Quantity<Length> distance)
      {
          return new DistanceFilter<>(DistanceOperatorName.WITHIN, library, geometry1, geometry2, distance);
      }
@@@ -632,9 -708,12 +632,9 @@@
       * @param  time1  expression fetching the first temporal value.
       * @param  time2  expression fetching the second temporal value.
       * @return a filter for the "After" operator between the two temporal values.
 -     *
 -     * @see TemporalOperatorName#AFTER
       */
-     public Filter<R> after(final Expression<? super R, ? extends T> time1,
-                            final Expression<? super R, ? extends T> time2)
 -    @Override
 -    public TemporalOperator<R> after(final Expression<R, ? extends T> time1,
 -                                     final Expression<R, ? extends T> time2)
++    public Filter<R> after(final Expression<R, ? extends T> time1,
++                           final Expression<R, ? extends T> time2)
      {
          return new TemporalFilter.After<>(time1, time2);
      }
@@@ -645,9 -724,12 +645,9 @@@
       * @param  time1  expression fetching the first temporal value.
       * @param  time2  expression fetching the second temporal value.
       * @return a filter for the "Before" operator between the two temporal values.
 -     *
 -     * @see TemporalOperatorName#BEFORE
       */
-     public Filter<R> before(final Expression<? super R, ? extends T> time1,
-                             final Expression<? super R, ? extends T> time2)
 -    @Override
 -    public TemporalOperator<R> before(final Expression<R, ? extends T> time1,
 -                                      final Expression<R, ? extends T> time2)
++    public Filter<R> before(final Expression<R, ? extends T> time1,
++                            final Expression<R, ? extends T> time2)
      {
          return new TemporalFilter.Before<>(time1, time2);
      }
@@@ -658,9 -740,12 +658,9 @@@
       * @param  time1  expression fetching the first temporal value.
       * @param  time2  expression fetching the second temporal value.
       * @return a filter for the "Begins" operator between the two temporal values.
 -     *
 -     * @see TemporalOperatorName#BEGINS
       */
-     public Filter<R> begins(final Expression<? super R, ? extends T> time1,
-                             final Expression<? super R, ? extends T> time2)
 -    @Override
 -    public TemporalOperator<R> begins(final Expression<R, ? extends T> time1,
 -                                      final Expression<R, ? extends T> time2)
++    public Filter<R> begins(final Expression<R, ? extends T> time1,
++                            final Expression<R, ? extends T> time2)
      {
          return new TemporalFilter.Begins<>(time1, time2);
      }
@@@ -671,9 -756,12 +671,9 @@@
       * @param  time1  expression fetching the first temporal value.
       * @param  time2  expression fetching the second temporal value.
       * @return a filter for the "BegunBy" operator between the two temporal values.
 -     *
 -     * @see TemporalOperatorName#BEGUN_BY
       */
-     public Filter<R> begunBy(final Expression<? super R, ? extends T> time1,
-                              final Expression<? super R, ? extends T> time2)
 -    @Override
 -    public TemporalOperator<R> begunBy(final Expression<R, ? extends T> time1,
 -                                       final Expression<R, ? extends T> time2)
++    public Filter<R> begunBy(final Expression<R, ? extends T> time1,
++                             final Expression<R, ? extends T> time2)
      {
          return new TemporalFilter.BegunBy<>(time1, time2);
      }
@@@ -684,9 -772,12 +684,9 @@@
       * @param  time1  expression fetching the first temporal value.
       * @param  time2  expression fetching the second temporal value.
       * @return a filter for the "TContains" operator between the two temporal values.
 -     *
 -     * @see TemporalOperatorName#CONTAINS
       */
-     public Filter<R> tcontains(final Expression<? super R, ? extends T> time1,
-                                final Expression<? super R, ? extends T> time2)
 -    @Override
 -    public TemporalOperator<R> tcontains(final Expression<R, ? extends T> time1,
 -                                         final Expression<R, ? extends T> time2)
++    public Filter<R> tcontains(final Expression<R, ? extends T> time1,
++                               final Expression<R, ? extends T> time2)
      {
          return new TemporalFilter.Contains<>(time1, time2);
      }
@@@ -697,9 -788,12 +697,9 @@@
       * @param  time1  expression fetching the first temporal value.
       * @param  time2  expression fetching the second temporal value.
       * @return a filter for the "During" operator between the two temporal values.
 -     *
 -     * @see TemporalOperatorName#DURING
       */
-     public Filter<R> during(final Expression<? super R, ? extends T> time1,
-                             final Expression<? super R, ? extends T> time2)
 -    @Override
 -    public TemporalOperator<R> during(final Expression<R, ? extends T> time1,
 -                                      final Expression<R, ? extends T> time2)
++    public Filter<R> during(final Expression<R, ? extends T> time1,
++                            final Expression<R, ? extends T> time2)
      {
          return new TemporalFilter.During<>(time1, time2);
      }
@@@ -710,9 -804,12 +710,9 @@@
       * @param  time1  expression fetching the first temporal value.
       * @param  time2  expression fetching the second temporal value.
       * @return a filter for the "TEquals" operator between the two temporal values.
 -     *
 -     * @see TemporalOperatorName#EQUALS
       */
-     public Filter<R> tequals(final Expression<? super R, ? extends T> time1,
-                              final Expression<? super R, ? extends T> time2)
 -    @Override
 -    public TemporalOperator<R> tequals(final Expression<R, ? extends T> time1,
 -                                       final Expression<R, ? extends T> time2)
++    public Filter<R> tequals(final Expression<R, ? extends T> time1,
++                             final Expression<R, ? extends T> time2)
      {
          return new TemporalFilter.Equals<>(time1, time2);
      }
@@@ -723,9 -820,12 +723,9 @@@
       * @param  time1  expression fetching the first temporal value.
       * @param  time2  expression fetching the second temporal value.
       * @return a filter for the "TOverlaps" operator between the two temporal values.
 -     *
 -     * @see TemporalOperatorName#OVERLAPS
       */
-     public Filter<R> toverlaps(final Expression<? super R, ? extends T> time1,
-                                final Expression<? super R, ? extends T> time2)
 -    @Override
 -    public TemporalOperator<R> toverlaps(final Expression<R, ? extends T> time1,
 -                                         final Expression<R, ? extends T> time2)
++    public Filter<R> toverlaps(final Expression<R, ? extends T> time1,
++                               final Expression<R, ? extends T> time2)
      {
          return new TemporalFilter.Overlaps<>(time1, time2);
      }
@@@ -736,9 -836,12 +736,9 @@@
       * @param  time1  expression fetching the first temporal value.
       * @param  time2  expression fetching the second temporal value.
       * @return a filter for the "Meets" operator between the two temporal values.
 -     *
 -     * @see TemporalOperatorName#MEETS
       */
-     public Filter<R> meets(final Expression<? super R, ? extends T> time1,
-                            final Expression<? super R, ? extends T> time2)
 -    @Override
 -    public TemporalOperator<R> meets(final Expression<R, ? extends T> time1,
 -                                     final Expression<R, ? extends T> time2)
++    public Filter<R> meets(final Expression<R, ? extends T> time1,
++                           final Expression<R, ? extends T> time2)
      {
          return new TemporalFilter.Meets<>(time1, time2);
      }
@@@ -749,9 -852,12 +749,9 @@@
       * @param  time1  expression fetching the first temporal value.
       * @param  time2  expression fetching the second temporal value.
       * @return a filter for the "Ends" operator between the two temporal values.
 -     *
 -     * @see TemporalOperatorName#ENDS
       */
-     public Filter<R> ends(final Expression<? super R, ? extends T> time1,
-                           final Expression<? super R, ? extends T> time2)
 -    @Override
 -    public TemporalOperator<R> ends(final Expression<R, ? extends T> time1,
 -                                    final Expression<R, ? extends T> time2)
++    public Filter<R> ends(final Expression<R, ? extends T> time1,
++                          final Expression<R, ? extends T> time2)
      {
          return new TemporalFilter.Ends<>(time1, time2);
      }
@@@ -762,9 -868,12 +762,9 @@@
       * @param  time1  expression fetching the first temporal value.
       * @param  time2  expression fetching the second temporal value.
       * @return a filter for the "OverlappedBy" operator between the two temporal values.
 -     *
 -     * @see TemporalOperatorName#OVERLAPPED_BY
       */
-     public Filter<R> overlappedBy(final Expression<? super R, ? extends T> time1,
-                                   final Expression<? super R, ? extends T> time2)
 -    @Override
 -    public TemporalOperator<R> overlappedBy(final Expression<R, ? extends T> time1,
 -                                            final Expression<R, ? extends T> time2)
++    public Filter<R> overlappedBy(final Expression<R, ? extends T> time1,
++                                  final Expression<R, ? extends T> time2)
      {
          return new TemporalFilter.OverlappedBy<>(time1, time2);
      }
@@@ -775,9 -884,12 +775,9 @@@
       * @param  time1  expression fetching the first temporal value.
       * @param  time2  expression fetching the second temporal value.
       * @return a filter for the "MetBy" operator between the two temporal values.
 -     *
 -     * @see TemporalOperatorName#MET_BY
       */
-     public Filter<R> metBy(final Expression<? super R, ? extends T> time1,
-                            final Expression<? super R, ? extends T> time2)
 -    @Override
 -    public TemporalOperator<R> metBy(final Expression<R, ? extends T> time1,
 -                                     final Expression<R, ? extends T> time2)
++    public Filter<R> metBy(final Expression<R, ? extends T> time1,
++                           final Expression<R, ? extends T> time2)
      {
          return new TemporalFilter.MetBy<>(time1, time2);
      }
@@@ -788,9 -900,12 +788,9 @@@
       * @param  time1  expression fetching the first temporal value.
       * @param  time2  expression fetching the second temporal value.
       * @return a filter for the "EndedBy" operator between the two temporal values.
 -     *
 -     * @see TemporalOperatorName#ENDED_BY
       */
-     public Filter<R> endedBy(final Expression<? super R, ? extends T> time1,
-                              final Expression<? super R, ? extends T> time2)
 -    @Override
 -    public TemporalOperator<R> endedBy(final Expression<R, ? extends T> time1,
 -                                       final Expression<R, ? extends T> time2)
++    public Filter<R> endedBy(final Expression<R, ? extends T> time1,
++                             final Expression<R, ? extends T> time2)
      {
          return new TemporalFilter.EndedBy<>(time1, time2);
      }
@@@ -802,9 -917,12 +802,9 @@@
       * @param  time1  expression fetching the first temporal value.
       * @param  time2  expression fetching the second temporal value.
       * @return a filter for the "AnyInteracts" operator between the two temporal values.
 -     *
 -     * @see TemporalOperatorName#ANY_INTERACTS
       */
-     public Filter<R> anyInteracts(final Expression<? super R, ? extends T> time1,
-                                   final Expression<? super R, ? extends T> time2)
 -    @Override
 -    public TemporalOperator<R> anyInteracts(final Expression<R, ? extends T> time1,
 -                                            final Expression<R, ? extends T> time2)
++    public Filter<R> anyInteracts(final Expression<R, ? extends T> time1,
++                                  final Expression<R, ? extends T> time2)
      {
          return new TemporalFilter.AnyInteracts<>(time1, time2);
      }
@@@ -815,9 -933,12 +815,9 @@@
       * @param  operand1  expression fetching the first number.
       * @param  operand2  expression fetching the second number.
       * @return an expression for the "Add" function between the two numerical values.
 -     *
 -     * @todo Should we really restrict the type to {@link Number}?
       */
-     public Expression<R,Number> add(final Expression<? super R, ? extends Number> operand1,
-                                     final Expression<? super R, ? extends Number> operand2)
 -    @Override
+     public Expression<R,Number> add(final Expression<R, ? extends Number> operand1,
+                                     final Expression<R, ? extends Number> operand2)
      {
          return new ArithmeticFunction.Add<>(operand1, operand2);
      }
@@@ -828,9 -949,12 +828,9 @@@
       * @param  operand1  expression fetching the first number.
       * @param  operand2  expression fetching the second number.
       * @return an expression for the "Subtract" function between the two numerical values.
 -     *
 -     * @todo Should we really restrict the type to {@link Number}?
       */
-     public Expression<R,Number> subtract(final Expression<? super R, ? extends Number> operand1,
-                                          final Expression<? super R, ? extends Number> operand2)
 -    @Override
+     public Expression<R,Number> subtract(final Expression<R, ? extends Number> operand1,
+                                          final Expression<R, ? extends Number> operand2)
      {
          return new ArithmeticFunction.Subtract<>(operand1, operand2);
      }
@@@ -841,9 -965,12 +841,9 @@@
       * @param  operand1  expression fetching the first number.
       * @param  operand2  expression fetching the second number.
       * @return an expression for the "Multiply" function between the two numerical values.
 -     *
 -     * @todo Should we really restrict the type to {@link Number}?
       */
-     public Expression<R,Number> multiply(final Expression<? super R, ? extends Number> operand1,
-                                          final Expression<? super R, ? extends Number> operand2)
 -    @Override
+     public Expression<R,Number> multiply(final Expression<R, ? extends Number> operand1,
+                                          final Expression<R, ? extends Number> operand2)
      {
          return new ArithmeticFunction.Multiply<>(operand1, operand2);
      }
@@@ -854,9 -981,12 +854,9 @@@
       * @param  operand1  expression fetching the first number.
       * @param  operand2  expression fetching the second number.
       * @return an expression for the "Divide" function between the two numerical values.
 -     *
 -     * @todo Should we really restrict the type to {@link Number}?
       */
-     public Expression<R,Number> divide(final Expression<? super R, ? extends Number> operand1,
-                                        final Expression<? super R, ? extends Number> operand2)
 -    @Override
+     public Expression<R,Number> divide(final Expression<R, ? extends Number> operand1,
+                                        final Expression<R, ? extends Number> operand2)
      {
          return new ArithmeticFunction.Divide<>(operand1, operand2);
      }
@@@ -899,7 -1001,8 +899,7 @@@
       * @throws IllegalArgumentException if the given name is not recognized,
       *         or if the arguments are illegal for the specified function.
       */
-     public Expression<R,?> function(final String name, Expression<? super R, ?>[] parameters) {
 -    @Override
+     public Expression<R,?> function(final String name, Expression<R,?>[] parameters) {
          ArgumentChecks.ensureNonNull("name", name);
          ArgumentChecks.ensureNonNull("parameters", parameters);
          parameters = parameters.clone();
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/DistanceFilter.java
index 76942d2db9,404770b4cd..30061a34c4
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/DistanceFilter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/DistanceFilter.java
@@@ -41,9 -43,9 +41,9 @@@ import org.apache.sis.internal.geoapi.f
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <G>  the implementation type of geometry objects.
   *
   * @since 1.1
@@@ -136,12 -139,13 +136,12 @@@ final class DistanceFilter<R,G> extend
       *
       * @throws IllegalStateException if the geometry is not a literal.
       */
 -    @Override
      public Geometry getGeometry() {
-         final Literal<? super R, ? extends GeometryWrapper<G>> literal;
+         final Literal<R, ? extends GeometryWrapper<G>> literal;
          if (expression2 instanceof Literal<?,?>) {
-             literal = (Literal<? super R, ? extends GeometryWrapper<G>>) expression2;
+             literal = (Literal<R, ? extends GeometryWrapper<G>>) expression2;
          } else if (expression1 instanceof Literal<?,?>) {
-             literal = (Literal<? super R, ? extends GeometryWrapper<G>>) expression1;
+             literal = (Literal<R, ? extends GeometryWrapper<G>>) expression1;
          } else {
              throw new IllegalStateException();
          }
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/Expression.java
index 3d28abfd48,0000000000..a55c9d1946
mode 100644,000000..100644
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/Expression.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/Expression.java
@@@ -1,74 -1,0 +1,83 @@@
 +/*
 + * Licensed to the Apache Software Foundation (ASF) under one or more
 + * contributor license agreements.  See the NOTICE file distributed with
 + * this work for additional information regarding copyright ownership.
 + * The ASF licenses this file to You under the Apache License, Version 2.0
 + * (the "License"); you may not use this file except in compliance with
 + * the License.  You may obtain a copy of the License at
 + *
 + *     http://www.apache.org/licenses/LICENSE-2.0
 + *
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS,
 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 + * See the License for the specific language governing permissions and
 + * limitations under the License.
 + */
 +package org.apache.sis.filter;
 +
 +import java.util.List;
 +import java.util.function.Function;
 +import org.opengis.util.ScopedName;
 +
 +
 +/**
 + * A literal or a named procedure that performs a distinct computation.
 + *
 + * <div class="warning"><b>Upcoming API change</b><br>
 + * This is a placeholder for a GeoAPI 3.1 interface not yet released.
 + * In a future version, all usages of this interface may be replaced
 + * by an interface of the same name but in the {@code org.opengis.filter} package
 + * instead of {@code org.apache.sis.filter}.
 + * </div>
 + *
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
 + * @param  <V>  the type of values computed by the expression.
 + */
 +public interface Expression<R,V> extends Function<R,V> {
 +    /**
 +     * Returns the name of the function to be called.
 +     *
 +     * @return name of the function to be called.
 +     */
 +    ScopedName getFunctionName();
 +
++    /**
++     * Returns the class of resources expected by this expression.
++     *
++     * @return type of resources accepted by this expression.
++     *
++     * @since 1.4
++     */
++    Class<? super R> getResourceClass();
++
 +    /**
 +     * Returns the list sub-expressions that will be evaluated to provide the parameters to the function.
 +     *
 +     * @return the sub-expressions to be evaluated, or an empty list if none.
 +     */
-     List<Expression<? super R, ?>> getParameters();
++    List<Expression<R,?>> getParameters();
 +
 +    /**
 +     * Evaluates the expression value based on the content of the given object.
 +     *
 +     * @param  input  the object to be evaluated by the expression.
 +     *         Can be {@code null} if this expression allows null values.
 +     * @return value computed by the expression.
 +     * @throws NullPointerException if {@code input} is null and this expression requires non-null values.
 +     * @throws IllegalArgumentException if the expression can not be applied on the given object.
 +     */
 +    @Override
 +    V apply(R input);
 +
 +    /**
 +     * Returns an expression doing the same evaluation than this method, but returning results
 +     * as values of the specified type.
 +     *
 +     * @param  <N>   compile-time value of {@code type}.
 +     * @param  type  desired type of expression results.
 +     * @return expression doing the same operation this this expression but with results of the specified type.
 +     * @throws ClassCastException if the specified type is not a target type supported by implementation.
 +     */
 +    <N> Expression<R,N> toValueType(Class<N> type);
 +}
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/Filter.java
index b35a5d7a1d,0000000000..1fd3741752
mode 100644,000000..100644
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/Filter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/Filter.java
@@@ -1,81 -1,0 +1,90 @@@
 +/*
 + * Licensed to the Apache Software Foundation (ASF) under one or more
 + * contributor license agreements.  See the NOTICE file distributed with
 + * this work for additional information regarding copyright ownership.
 + * The ASF licenses this file to You under the Apache License, Version 2.0
 + * (the "License"); you may not use this file except in compliance with
 + * the License.  You may obtain a copy of the License at
 + *
 + *     http://www.apache.org/licenses/LICENSE-2.0
 + *
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS,
 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 + * See the License for the specific language governing permissions and
 + * limitations under the License.
 + */
 +package org.apache.sis.filter;
 +
 +import java.util.List;
 +import java.util.function.Predicate;
 +
 +
 +/**
 + * Identification of a subset of resources from a collection of resources
 + * whose property values satisfy a set of logically connected predicates.
 + *
 + * <div class="warning"><b>Upcoming API change</b><br>
 + * This is a placeholder for a GeoAPI 3.1 interface not yet released.
 + * In a future version, all usages of this interface may be replaced
 + * by an interface of the same name but in the {@code org.opengis.filter} package
 + * instead of {@code org.apache.sis.filter}.
 + * </div>
 + */
 +public interface Filter<R> extends Predicate<R> {
 +    /**
 +     * A filter that always evaluates to {@code true}.
 +     *
 +     * @param  <R>  the type of resources to filter.
 +     * @return the "no filtering" filter.
 +     */
 +    @SuppressWarnings("unchecked")
 +    static <R> Filter<R> include() {
 +        return FilterLiteral.INCLUDE;
 +    }
 +
 +    /**
 +     * A filter that always evaluates to {@code false}.
 +     *
 +     * @param  <R>  the type of resources to filter.
 +     * @return the "exclude all" filter.
 +     */
 +    @SuppressWarnings("unchecked")
 +    static <R> Filter<R> exclude() {
 +        return FilterLiteral.EXCLUDE;
 +    }
 +
 +    /**
 +     * Returns the nature of the operator.
 +     *
 +     * @return the nature of this operator.
 +     */
 +    Enum<?> getOperatorType();
 +
++    /**
++     * Returns the class of resources expected by this filter.
++     *
++     * @return type of resources accepted by this filter.
++     *
++     * @since 1.4
++     */
++    Class<? super R> getResourceClass();
++
 +    /**
 +     * Returns the expressions used as arguments for this filter.
 +     *
 +     * @return the expressions used as inputs, or an empty list if none.
 +     */
-     List<Expression<? super R, ?>> getExpressions();
++    List<Expression<R,?>> getExpressions();
 +
 +    /**
 +     * Given an object, determines if the test(s) represented by this filter are passed.
 +     *
 +     * @param  object  the object (often a {@code Feature} instance) to evaluate.
 +     * @return {@code true} if the test(s) are passed for the provided object.
 +     * @throws NullPointerException if {@code object} is null.
 +     * @throws IllegalArgumentException if the filter can not be applied on the given object.
 +     */
 +    @Override
 +    boolean test(R object);
 +}
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/FilterLiteral.java
index 8ca4b9c401,0000000000..d39cef550c
mode 100644,000000..100644
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/FilterLiteral.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/FilterLiteral.java
@@@ -1,65 -1,0 +1,70 @@@
 +/*
 + * Licensed to the Apache Software Foundation (ASF) under one or more
 + * contributor license agreements.  See the NOTICE file distributed with
 + * this work for additional information regarding copyright ownership.
 + * The ASF licenses this file to You under the Apache License, Version 2.0
 + * (the "License"); you may not use this file except in compliance with
 + * the License.  You may obtain a copy of the License at
 + *
 + *     http://www.apache.org/licenses/LICENSE-2.0
 + *
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS,
 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 + * See the License for the specific language governing permissions and
 + * limitations under the License.
 + */
 +package org.apache.sis.filter;
 +
 +import java.util.List;
 +import java.util.Collections;
 +import java.io.Serializable;
 +import java.io.ObjectStreamException;
 +
 +
 +/**
 + * Placeholder for GeoAPI 3.1 interfaces (not yet released).
 + * Shall not be visible in public API, as it will be deleted after next GeoAPI release.
 + */
 +final class FilterLiteral implements Filter<Object>, Serializable {
 +    @SuppressWarnings("rawtypes")
 +    public static final Filter INCLUDE = new FilterLiteral(true);
 +
 +    @SuppressWarnings("rawtypes")
 +    public static final Filter EXCLUDE = new FilterLiteral(false);
 +
 +    private final boolean value;
 +
 +    private FilterLiteral(final boolean value) {
 +        this.value = value;
 +    }
 +
 +    @Override
 +    public Enum<?> getOperatorType() {
 +        return value ? FilterName.INCLUDE : FilterName.EXCLUDE;
 +    }
 +
++    @Override
++    public Class<Object> getResourceClass() {
++        return Object.class;
++    }
++
 +    @Override
 +    public List<Expression<? super Object, ?>> getExpressions() {
 +        return Collections.emptyList();
 +    }
 +
 +    @Override
 +    public boolean test(Object object) {
 +        return value;
 +    }
 +
 +    @Override
 +    public String toString() {
 +        return "Filter." + (value ? "INCLUDE" : "EXCLUDE");
 +    }
 +
 +    private Object readResolve() throws ObjectStreamException {
 +        return value ? INCLUDE : EXCLUDE;
 +    }
 +}
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/IdentifierFilter.java
index f47c13d287,102fc4363e..9017aaab4d
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/IdentifierFilter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/IdentifierFilter.java
@@@ -31,13 -35,10 +32,10 @@@ import org.apache.sis.feature.AbstractF
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
-  *
-  * @param  <R>  the type of resources used as inputs.
-  *
-  * @since 1.1
+  * @version 1.4
+  * @since   1.1
   */
- final class IdentifierFilter<R extends AbstractFeature> extends FilterNode<R> {
 -final class IdentifierFilter extends Node implements ResourceId<Feature>, Optimization.OnFilter<Feature> {
++final class IdentifierFilter extends Node implements Optimization.OnFilter<AbstractFeature> {
      /**
       * For cross-version compatibility.
       */
@@@ -56,11 -57,23 +54,28 @@@
          this.identifier = identifier;
      }
  
 +    @Override
 +    public Enum<?> getOperatorType() {
 +        return FilterName.RESOURCE_ID;
 +    }
 +
+     /**
+      * Nothing to optimize here. The {@code Optimization.OnFilter} interface
+      * is implemented for inheriting the AND, OR and NOT methods overriding.
+      */
+     @Override
 -    public Filter<Feature> optimize(Optimization optimization) {
++    public Filter<AbstractFeature> optimize(Optimization optimization) {
+         return this;
+     }
+ 
+     /**
+      * Returns the class of resources expected by this expression.
+      */
+     @Override
 -    public Class<Feature> getResourceClass() {
 -        return Feature.class;
++    public Class<AbstractFeature> getResourceClass() {
++        return AbstractFeature.class;
+     }
+ 
      /**
       * Returns the identifiers of feature instances to accept.
       */
@@@ -72,7 -86,7 +87,7 @@@
       * Returns the parameters of this filter.
       */
      @Override
-     public List<Expression<? super R, ?>> getExpressions() {
 -    public List<Expression<Feature,?>> getExpressions() {
++    public List<Expression<AbstractFeature,?>> getExpressions() {
          return List.of(new LeafExpression.Literal<>(identifier));
      }
  
@@@ -90,7 -104,7 +105,7 @@@
       * is one of the identifier specified at {@code IdentifierFilter} construction time.
       */
      @Override
-     public boolean test(R object) {
 -    public boolean test(final Feature object) {
++    public boolean test(final AbstractFeature object) {
          if (object == null) {
              return false;
          }
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/LeafExpression.java
index 4bd0256975,18e6864560..aac4113826
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/LeafExpression.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/LeafExpression.java
@@@ -41,9 -42,9 +41,9 @@@ import org.apache.sis.feature.DefaultAt
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <V>  the type of value computed by the expression.
   *
   * @since 1.1
@@@ -92,6 -93,6 +92,10 @@@ abstract class LeafExpression<R,V> exte
              this.value = value;             // Null is accepted.
          }
  
++        @Override public Class<? super R> getResourceClass() {
++            return Object.class;
++        }
++
          /** For {@link #toString()}, {@link #hashCode()} and {@link #equals(Object)} implementations. */
          @Override protected Collection<?> getChildren() {
              // Not `List.of(…)` because value may be null.
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/LikeFilter.java
index 7e8e569dae,bc3a4ab79e..88720d0fa1
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/LikeFilter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/LikeFilter.java
@@@ -20,9 -20,12 +20,10 @@@ import java.util.List
  import java.util.Collection;
  import java.util.regex.Pattern;
  import org.apache.sis.util.ArgumentChecks;
+ import org.apache.sis.internal.filter.Node;
  
  // Branch-dependent imports
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.LikeOperator;
 +import org.apache.sis.internal.geoapi.filter.ComparisonOperatorName;
  
  
  /**
@@@ -30,13 -33,13 +31,13 @@@
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   *
   * @since 1.1
   */
- final class LikeFilter<R> extends FilterNode<R> implements Optimization.OnFilter<R> {
 -final class LikeFilter<R> extends Node implements LikeOperator<R>, Optimization.OnFilter<R> {
++final class LikeFilter<R> extends Node implements Optimization.OnFilter<R> {
      /**
       * For cross-version compatibility.
       */
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/LogicalFilter.java
index 6805ddfadf,8dc2b23b28..cd2148b839
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/LogicalFilter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/LogicalFilter.java
@@@ -34,9 -36,9 +35,9 @@@ import org.apache.sis.internal.geoapi.f
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.2
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   *
   * @since 1.1
   */
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java
index 365831f00f,cae63556d3..825fc7393b
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/PropertyValue.java
@@@ -138,6 -133,14 +138,14 @@@ split:  if (path != null) 
          return (path == null || path.isEmpty()) ? tip : new AssociationValue<>(path, tip);
      }
  
+     /**
+      * Returns the class of resources expected by this expression.
+      */
+     @Override
 -    public final Class<Feature> getResourceClass() {
 -        return Feature.class;
++    public final Class<AbstractFeature> getResourceClass() {
++        return AbstractFeature.class;
+     }
+ 
      /**
       * For {@link #toString()}, {@link #hashCode()} and {@link #equals(Object)} implementations.
       */
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/TemporalFilter.java
index aa6d0d3175,c38677e51c..d66a1089a6
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/TemporalFilter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/TemporalFilter.java
@@@ -37,9 -40,9 +37,9 @@@ import org.apache.sis.internal.geoapi.f
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <T>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <T>  the type of resources (e.g. {@code Feature}) used as inputs.
   *
   * @since 1.1
   */
diff --cc core/sis-feature/src/main/java/org/apache/sis/filter/UnaryFunction.java
index 451f522112,cff832db14..c25b3165fb
--- a/core/sis-feature/src/main/java/org/apache/sis/filter/UnaryFunction.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/filter/UnaryFunction.java
@@@ -33,9 -36,9 +33,9 @@@ import org.apache.sis.internal.geoapi.f
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <V>  the type of value computed by the expression.
   *
   * @since 1.1
@@@ -108,12 -121,8 +118,12 @@@ class UnaryFunction<R,V> extends Node 
              super(expression);
          }
  
 +        @Override public ComparisonOperatorName getOperatorType() {
 +            return ComparisonOperatorName.PROPERTY_IS_NULL;
 +        }
 +
          /** Creates a new filter of the same type but different parameters. */
-         @Override public Filter<R> recreate(final Expression<? super R, ?>[] effective) {
+         @Override public Filter<R> recreate(final Expression<R,?>[] effective) {
              return new IsNull<>(effective[0]);
          }
  
@@@ -151,12 -160,8 +161,12 @@@
              this.nilReason = nilReason;
          }
  
 +        @Override public ComparisonOperatorName getOperatorType() {
 +            return ComparisonOperatorName.PROPERTY_IS_NIL;
 +        }
 +
          /** Creates a new filter of the same type but different parameters. */
-         @Override public Filter<R> recreate(final Expression<? super R, ?>[] effective) {
+         @Override public Filter<R> recreate(final Expression<R,?>[] effective) {
              return new IsNil<>(effective[0], nilReason);
          }
  
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/filter/GeometryConverter.java
index 211d7f7c79,a9bcf55dc4..b3a4a96bd7
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/GeometryConverter.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/GeometryConverter.java
@@@ -42,9 -43,9 +42,9 @@@ import org.apache.sis.filter.Expression
   *
   * @author  Martin Desruisseaux (Geomatys)
   * @author  Alexis Manin (Geomatys)
-  * @version 1.3
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <G>  the geometry implementation type.
   *
   * @see org.apache.sis.filter.ConvertFunction
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/filter/Node.java
index 67ee11a650,581588efb3..5e615957e1
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/Node.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/Node.java
@@@ -88,12 -90,32 +88,32 @@@ public abstract class Node implements S
       *
       * @see Expression#getFunctionName()
       */
 -    protected static <T> AttributeType<T> createType(final Class<T> type, final Object name) {
 +    protected static <T> DefaultAttributeType<T> createType(final Class<T> type, final Object name) {
          // We do not use `Map.of(…)` for letting the attribute type constructor do the null check.
          return new DefaultAttributeType<>(Collections.singletonMap(DefaultAttributeType.NAME_KEY, name),
 -                                          type, 1, 1, null, (AttributeType<?>[]) null);
 +                                          type, 1, 1, null, (DefaultAttributeType<?>[]) null);
      }
  
+     /**
+      * Returns the most specialized class of the given pair of class. A specialized class is guaranteed to exist
+      * if parametrized type safety has not been bypassed with unchecked casts, because {@code <R>} is always valid.
+      * However this method is not guaranteed to be able to find that specialized type, because it could be none of
+      * the given arguments if {@code t1}, {@code t2} and {@code <R>} are interfaces with {@code <R>} extending both
+      * {@code t1} and {@code t2}.
+      *
+      * @param  <R>  the compile-time type of resources expected by filters or expressions.
+      * @param  t1   the runtime type of resources expected by the first filter or expression. May be null.
+      * @param  t2   the runtime type of resources expected by the second filter or expression. May be null.
+      * @return the most specialized type of resources, or {@code null} if it cannot be determined.
+      */
+     protected static <R> Class<? super R> specializedClass(final Class<? super R> t1, final Class<? super R> t2) {
+         if (t1 != null && t2 != null) {
+             if (t1.isAssignableFrom(t2)) return t2;
+             if (t2.isAssignableFrom(t1)) return t1;
+         }
+         return null;
+     }
+ 
      /**
       * Returns the mathematical symbol for this binary function.
       * For comparison operators, the symbol should be one of {@literal < > ≤ ≥ = ≠}.
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/filter/Visitor.java
index 9c47f9c5a0,60945dfea5..dc879f081e
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/Visitor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/Visitor.java
@@@ -24,14 -24,14 +24,13 @@@ import java.util.function.BiConsumer
  import org.apache.sis.internal.feature.Resources;
  
  // Branch-dependent imports
 -import org.opengis.filter.Filter;
 -import org.opengis.filter.Expression;
 -import org.opengis.filter.LogicalOperatorName;
 -import org.opengis.filter.SpatialOperatorName;
 -import org.opengis.filter.DistanceOperatorName;
 -import org.opengis.filter.TemporalOperatorName;
 -import org.opengis.filter.ComparisonOperatorName;
 +import org.apache.sis.filter.Filter;
 +import org.apache.sis.filter.Expression;
- import org.apache.sis.internal.geoapi.filter.LogicalOperator;
 +import org.apache.sis.internal.geoapi.filter.LogicalOperatorName;
 +import org.apache.sis.internal.geoapi.filter.SpatialOperatorName;
 +import org.apache.sis.internal.geoapi.filter.DistanceOperatorName;
 +import org.apache.sis.internal.geoapi.filter.TemporalOperatorName;
 +import org.apache.sis.internal.geoapi.filter.ComparisonOperatorName;
  
  
  /**
@@@ -49,9 -49,9 +48,9 @@@
   * {@code Visitor} instances are thread-safe if protected methods are invoked at construction time only.
   *
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <A>  type of the accumulator object where actions will write their results.
   *
   * @since 1.1
@@@ -259,10 -259,10 +258,10 @@@ public abstract class Visitor<R,A> 
       * Actions are registered by calls to {@code setFooHandler(…)} before the call to this {@code visit(…)} method.
       *
       * <h4>Note on parameterized type</h4>
-      * This method often needs to be invoked with instances of {@code Filter<? super R>},
-      * because this is the type of filters returned by GeoAPI methods such as {@link LogicalOperator#getOperands()}.
+      * This method sometimes needs to be invoked with instances of {@code Filter<? super R>},
+      * because this is the type of predicate expected by {@link java.util.function} and {@link java.util.stream}.
       * But the parameterized type expected by this method matches the parameterized type of handlers registered by
 -     * {@link #setFilterHandler(CodeList, BiConsumer)} and similar methods, which use the exact {@code <R>} type.
 +     * {@link #setFilterHandler(Enum, BiConsumer)} and similar methods, which use the exact {@code <R>} type.
       * This restriction exists because when doing otherwise, parameterized types become hard to express in Java
       * (we get a cascade of {@code super} keywords, something like {@code <? super ? super R>}).
       * However, doing the {@code (Filter<R>) filter} cast is actually safe if the handlers do not invoke any
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/FunctionWithSRID.java
index 7f3a0d4b1c,b2640be641..2edc6d4b4b
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/FunctionWithSRID.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/FunctionWithSRID.java
@@@ -43,9 -44,9 +43,9 @@@ import org.apache.sis.internal.geoapi.f
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
   * @author  Alexis Manin (Geomatys)
-  * @version 1.3
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   *
   * @since 1.1
   */
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/GeometryConstructor.java
index c88d01cbac,bebef3918e..07e52584a7
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/GeometryConstructor.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/GeometryConstructor.java
@@@ -34,9 -35,9 +34,9 @@@ import org.apache.sis.filter.Expression
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <G>  the implementation type of geometry objects.
   *
   * @since 1.1
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/GeometryParser.java
index c84813e4da,425f02a9c8..635882b939
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/GeometryParser.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/GeometryParser.java
@@@ -32,9 -33,9 +32,9 @@@ import org.apache.sis.filter.Expression
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <G>  the implementation type of geometry objects.
   *
   * @since 1.1
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/OneGeometry.java
index 2b8774708e,2ce129b57d..5f1da923c3
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/OneGeometry.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/OneGeometry.java
@@@ -31,9 -31,9 +31,9 @@@ import org.apache.sis.filter.Expression
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.3
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <G>  the implementation type of geometry objects.
   *
   * @since 1.1
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/ST_FromBinary.java
index 22d64ab43d,ef835b17c7..b76ddbf5dd
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/ST_FromBinary.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/ST_FromBinary.java
@@@ -29,9 -29,9 +29,9 @@@ import org.apache.sis.filter.Expression
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <G>  the implementation type of geometry objects.
   *
   * @since 1.1
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/ST_FromText.java
index 8608b18868,e8f919dedc..40e20e4904
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/ST_FromText.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/ST_FromText.java
@@@ -28,9 -28,9 +28,9 @@@ import org.apache.sis.filter.Expression
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <G>  the implementation type of geometry objects.
   *
   * @since 1.1
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/ST_Point.java
index 028dbacd98,eeb2809a65..94f90443f9
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/ST_Point.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/ST_Point.java
@@@ -44,9 -45,9 +44,9 @@@ import org.apache.sis.filter.Expression
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <G>  the implementation type of geometry objects.
   *
   * @since 1.1
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/ST_Transform.java
index 9e2c9ec4ce,887ae43dc4..2370a485a4
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/ST_Transform.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/ST_Transform.java
@@@ -76,9 -77,9 +76,9 @@@ final class ST_Transform<R,G> extends F
       * Creates a new function with the given parameters. It is caller's responsibility to ensure
       * that the given array is non-null and does not contain null elements.
       *
 -     * @throws InvalidFilterValueException if CRS cannot be constructed from the second expression.
 +     * @throws IllegalArgumentException if CRS cannot be constructed from the second expression.
       */
-     ST_Transform(final Expression<? super R, ?>[] parameters, final Geometries<G> library) {
+     ST_Transform(final Expression<R,?>[] parameters, final Geometries<G> library) {
          super(SQLMM.ST_Transform, parameters, PRESENT);
          geometry = toGeometryWrapper(library, parameters[0]);
      }
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/TwoGeometries.java
index a60e2744ce,ad06f76fa8..cc72136844
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/TwoGeometries.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/filter/sqlmm/TwoGeometries.java
@@@ -36,9 -37,9 +36,9 @@@ import org.apache.sis.internal.geoapi.f
   *
   * @author  Johann Sorel (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.3
+  * @version 1.4
   *
 - * @param  <R>  the type of resources (e.g. {@link org.opengis.feature.Feature}) used as inputs.
 + * @param  <R>  the type of resources (e.g. {@code Feature}) used as inputs.
   * @param  <G>  the implementation type of geometry objects.
   *
   * @since 1.1
@@@ -80,10 -81,10 +80,10 @@@ class TwoGeometries<R,G> extends Spatia
       * executed in the CRS of the first argument.
       */
      @Override
-     public Expression<? super R, ?> optimize(final Optimization optimization) {
+     public Expression<R,?> optimize(final Optimization optimization) {
 -        final FeatureType featureType = optimization.getFeatureType();
 +        final DefaultFeatureType featureType = optimization.getFeatureType();
          if (featureType != null) {
-             final Expression<? super R, ?> p1 = unwrap(geometry1);
+             final Expression<R,?> p1 = unwrap(geometry1);
              if (p1 instanceof ValueReference<?,?> && unwrap(geometry2) instanceof Literal<?,?>) try {
                  final CoordinateReferenceSystem targetCRS = AttributeConvention.getCRSCharacteristic(
                          featureType, featureType.getProperty(((ValueReference<?,?>) p1).getXPath()));
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/BetweenComparisonOperator.java
index 8e4e8f30be,18427e2c07..2905fb62c0
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/BetweenComparisonOperator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/BetweenComparisonOperator.java
@@@ -14,18 -14,23 +14,18 @@@
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
 -package org.apache.sis.internal.map;
 +package org.apache.sis.internal.geoapi.filter;
 +
 +import org.apache.sis.filter.Expression;
 +import org.apache.sis.filter.Filter;
  
 -import org.opengis.style.Symbolizer;
  
  /**
 - * Resource symbolizers act on a resource as a whole, not on individual features.
 - * Such symbolizers are not defined by the Symbology Encoding specification but are
 - * often required to produce uncommon presentations.
 - *
 - * <p>
 - * NOTE: this class is a first draft subject to modifications.
 - * </p>
 - *
 - * @author  Johann Sorel (Geomatys)
 - * @version 1.2
 - * @since   1.2
 + * Placeholder for GeoAPI 3.1 interfaces (not yet released).
 + * Shall not be visible in public API, as it will be deleted after next GeoAPI release.
   */
 -public interface ResourceSymbolizer extends Symbolizer {
 -
 +public interface BetweenComparisonOperator<R> extends Filter<R> {
-     Expression<? super R, ?> getExpression();
-     Expression<? super R, ?> getLowerBoundary();
-     Expression<? super R, ?> getUpperBoundary();
++    Expression<R,?> getExpression();
++    Expression<R,?> getLowerBoundary();
++    Expression<R,?> getUpperBoundary();
  }
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/BinaryComparisonOperator.java
index b2fff0f701,18427e2c07..eec80796c3
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/BinaryComparisonOperator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/BinaryComparisonOperator.java
@@@ -14,19 -14,23 +14,19 @@@
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
 -package org.apache.sis.internal.map;
 +package org.apache.sis.internal.geoapi.filter;
 +
 +import org.apache.sis.filter.Expression;
 +import org.apache.sis.filter.Filter;
  
 -import org.opengis.style.Symbolizer;
  
  /**
 - * Resource symbolizers act on a resource as a whole, not on individual features.
 - * Such symbolizers are not defined by the Symbology Encoding specification but are
 - * often required to produce uncommon presentations.
 - *
 - * <p>
 - * NOTE: this class is a first draft subject to modifications.
 - * </p>
 - *
 - * @author  Johann Sorel (Geomatys)
 - * @version 1.2
 - * @since   1.2
 + * Placeholder for GeoAPI 3.1 interfaces (not yet released).
 + * Shall not be visible in public API, as it will be deleted after next GeoAPI release.
   */
 -public interface ResourceSymbolizer extends Symbolizer {
 -
 +public interface BinaryComparisonOperator<R> extends Filter<R> {
-     Expression<? super R, ?> getOperand1();
-     Expression<? super R, ?> getOperand2();
++    Expression<R,?> getOperand1();
++    Expression<R,?> getOperand2();
 +    boolean isMatchingCase();
 +    MatchAction getMatchAction();
  }
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/FilterExpressions.java
index 6bb16907b0,0000000000..cae55c6df5
mode 100644,000000..100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/FilterExpressions.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/FilterExpressions.java
@@@ -1,105 -1,0 +1,110 @@@
 +/*
 + * Licensed to the Apache Software Foundation (ASF) under one or more
 + * contributor license agreements.  See the NOTICE file distributed with
 + * this work for additional information regarding copyright ownership.
 + * The ASF licenses this file to You under the Apache License, Version 2.0
 + * (the "License"); you may not use this file except in compliance with
 + * the License.  You may obtain a copy of the License at
 + *
 + *     http://www.apache.org/licenses/LICENSE-2.0
 + *
 + * Unless required by applicable law or agreed to in writing, software
 + * distributed under the License is distributed on an "AS IS" BASIS,
 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 + * See the License for the specific language governing permissions and
 + * limitations under the License.
 + */
 +package org.apache.sis.internal.geoapi.filter;
 +
 +import java.util.List;
 +import java.util.Objects;
 +import java.util.AbstractList;
 +import java.util.Locale;
 +import org.opengis.util.ScopedName;
 +import org.apache.sis.filter.Filter;
 +import org.apache.sis.filter.Expression;
 +import org.apache.sis.util.iso.Names;
 +
 +
 +/**
 + * Placeholder for GeoAPI 3.1 interfaces (not yet released).
 + * Shall not be visible in public API, as it will be deleted after next GeoAPI release.
 + */
- final class FilterExpressions<R> extends AbstractList<Expression<? super R, ?>> {
-     private final List<Filter<? super R>> filters;
++final class FilterExpressions<R> extends AbstractList<Expression<R,?>> {
++    private final List<Filter<R>> filters;
 +
-     FilterExpressions(final List<Filter<? super R>> filters) {
++    FilterExpressions(final List<Filter<R>> filters) {
 +        this.filters = Objects.requireNonNull(filters);
 +    }
 +
 +    @Override
 +    public boolean isEmpty() {
 +        return filters.isEmpty();
 +    }
 +
 +    @Override
 +    public int size() {
 +        return filters.size();
 +    }
 +
 +    @Override
-     public Expression<? super R, ?> get(final int index) {
++    public Expression<R,?> get(final int index) {
 +        return new Element<>(filters.get(index));
 +    }
 +
 +    private static final class Element<R> implements Expression<R,Boolean> {
 +        private final Filter<R> filter;
 +
 +        Element(final Filter<R> filter) {
 +            this.filter = Objects.requireNonNull(filter);
 +        }
 +
 +        @Override
 +        public ScopedName getFunctionName() {
 +            final Enum<?> type = filter.getOperatorType();
 +            final String identifier = type.name().toLowerCase(Locale.US);
 +            if (identifier != null) {
 +                return Names.createScopedName(Name.STANDARD, null, identifier);
 +            } else {
 +                return Names.createScopedName(Name.EXTENSION, null, type.name());
 +            }
 +        }
 +
 +        @Override
-         public List<Expression<? super R, ?>> getParameters() {
++        public Class<? super R> getResourceClass() {
++            return filter.getResourceClass();
++        }
++
++        @Override
++        public List<Expression<R,?>> getParameters() {
 +            return filter.getExpressions();
 +        }
 +
 +        @Override
 +        public Boolean apply(final R input) {
 +            return filter.test(input);
 +        }
 +
 +        @Override
 +        @SuppressWarnings("unchecked")
 +        public <N> Expression<R,N> toValueType(final Class<N> type) {
 +            if (type.isAssignableFrom(Boolean.class)) return (Expression<R,N>) this;
 +            else throw new ClassCastException();
 +        }
 +
 +        @Override
 +        public int hashCode() {
 +            return ~filter.hashCode();
 +        }
 +
 +        @Override
 +        public boolean equals(final Object obj) {
 +            return (obj instanceof Element) && filter.equals(((Element) obj).filter);
 +        }
 +
 +        @Override
 +        public String toString() {
 +            return "Expression[" + filter.toString() + ']';
 +        }
 +    }
 +}
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/Literal.java
index 85d0294458,18427e2c07..93c3140420
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/Literal.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/Literal.java
@@@ -14,28 -14,23 +14,28 @@@
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
 -package org.apache.sis.internal.map;
 +package org.apache.sis.internal.geoapi.filter;
 +
 +import java.util.List;
 +import java.util.Collections;
 +import org.opengis.util.ScopedName;
 +import org.apache.sis.filter.Expression;
  
 -import org.opengis.style.Symbolizer;
  
  /**
 - * Resource symbolizers act on a resource as a whole, not on individual features.
 - * Such symbolizers are not defined by the Symbology Encoding specification but are
 - * often required to produce uncommon presentations.
 - *
 - * <p>
 - * NOTE: this class is a first draft subject to modifications.
 - * </p>
 - *
 - * @author  Johann Sorel (Geomatys)
 - * @version 1.2
 - * @since   1.2
 + * Placeholder for GeoAPI 3.1 interfaces (not yet released).
 + * Shall not be visible in public API, as it will be deleted after next GeoAPI release.
   */
 -public interface ResourceSymbolizer extends Symbolizer {
 +public interface Literal<R,V> extends Expression<R,V> {
 +    @Override
 +    default ScopedName getFunctionName() {
 +        return Name.LITERAL;
 +    }
 +
 +    @Override
-     default List<Expression<? super R, ?>> getParameters() {
++    default List<Expression<R,?>> getParameters() {
 +        return Collections.emptyList();
 +    }
  
 +    V getValue();
  }
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/LogicalOperator.java
index 8647cd1ed3,18427e2c07..e68663026b
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/LogicalOperator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/LogicalOperator.java
@@@ -14,25 -14,23 +14,25 @@@
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
 -package org.apache.sis.internal.map;
 +package org.apache.sis.internal.geoapi.filter;
 +
 +import java.util.List;
 +import org.apache.sis.filter.Filter;
 +import org.apache.sis.filter.Expression;
  
 -import org.opengis.style.Symbolizer;
  
  /**
 - * Resource symbolizers act on a resource as a whole, not on individual features.
 - * Such symbolizers are not defined by the Symbology Encoding specification but are
 - * often required to produce uncommon presentations.
 - *
 - * <p>
 - * NOTE: this class is a first draft subject to modifications.
 - * </p>
 - *
 - * @author  Johann Sorel (Geomatys)
 - * @version 1.2
 - * @since   1.2
 + * Placeholder for GeoAPI 3.1 interfaces (not yet released).
 + * Shall not be visible in public API, as it will be deleted after next GeoAPI release.
   */
 -public interface ResourceSymbolizer extends Symbolizer {
 +public interface LogicalOperator<R> extends Filter<R> {
 +    @Override
 +    LogicalOperatorName getOperatorType();
 +
 +    @Override
-     default List<Expression<? super R, ?>> getExpressions() {
++    default List<Expression<R,?>> getExpressions() {
 +        return new FilterExpressions<>(getOperands());
 +    }
  
-     List<Filter<? super R>> getOperands();
++    List<Filter<R>> getOperands();
  }
diff --cc core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/SortProperty.java
index ab27a0e692,18427e2c07..f3cd74aa69
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/SortProperty.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/geoapi/filter/SortProperty.java
@@@ -14,17 -14,23 +14,17 @@@
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
 -package org.apache.sis.internal.map;
 +package org.apache.sis.internal.geoapi.filter;
 +
 +import java.util.Comparator;
  
 -import org.opengis.style.Symbolizer;
  
  /**
 - * Resource symbolizers act on a resource as a whole, not on individual features.
 - * Such symbolizers are not defined by the Symbology Encoding specification but are
 - * often required to produce uncommon presentations.
 - *
 - * <p>
 - * NOTE: this class is a first draft subject to modifications.
 - * </p>
 - *
 - * @author  Johann Sorel (Geomatys)
 - * @version 1.2
 - * @since   1.2
 + * Placeholder for GeoAPI 3.1 interfaces (not yet released).
 + * Shall not be visible in public API, as it will be deleted after next GeoAPI release.
   */
 -public interface ResourceSymbolizer extends Symbolizer {
 +public interface SortProperty<R> extends Comparator<R> {
-     ValueReference<? super R, ?> getValueReference();
++    ValueReference<R,?> getValueReference();
  
 +    SortOrder getSortOrder();
  }
diff --cc core/sis-feature/src/test/java/org/apache/sis/filter/BinarySpatialFilterTestCase.java
index 6b21237346,9256708be1..5b1c0bc185
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/BinarySpatialFilterTestCase.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/BinarySpatialFilterTestCase.java
@@@ -131,10 -134,11 +131,11 @@@ public abstract class BinarySpatialFilt
       */
      @Test
      public void bbox_preserve_expression_type() {
 -        final BinarySpatialOperator<Feature> bbox = factory.bbox(literal(Polygon.RIGHT), new Envelope2D(null, 0, 0, 1, 1));
 -        final Expression<Feature,?> arg2 = bbox.getOperand2();
 +        final Filter<AbstractFeature> bbox = factory.bbox(literal(Polygon.RIGHT), new Envelope2D(null, 0, 0, 1, 1));
-         final Expression<? super AbstractFeature, ?> arg2 = bbox.getExpressions().get(1);
++        final Expression<AbstractFeature,?> arg2 = bbox.getExpressions().get(1);
+         assertSame("The two ways to acquire the second argument return different values.", arg2, bbox.getExpressions().get(1));
          assertInstanceOf("Second argument value should be an envelope.", Envelope.class,
-                          ((Literal<? super AbstractFeature, ?>) arg2).getValue());
 -                         ((Literal<Feature,?>) arg2).getValue());
++                         ((Literal<AbstractFeature,?>) arg2).getValue());
      }
  
      /**
diff --cc core/sis-feature/src/test/java/org/apache/sis/filter/IdentifierFilterTest.java
index 0552ce6a27,d005888119..e08dbc2e1f
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/IdentifierFilterTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/IdentifierFilterTest.java
@@@ -73,12 -75,13 +73,13 @@@ public final class IdentifierFilterTes
          f1.setPropertyValue("att", "123");
  
          ftb.clear().addAttribute(Integer.class).setName("att").addRole(AttributeRole.IDENTIFIER_COMPONENT);
 -        final Feature f2 = ftb.setName("Test 2").build().newInstance();
 +        final AbstractFeature f2 = ftb.setName("Test 2").build().newInstance();
          f2.setPropertyValue("att", 123);
  
 -        final Feature f3 = ftb.clear().setName("Test 3").build().newInstance();
 +        final AbstractFeature f3 = ftb.clear().setName("Test 3").build().newInstance();
  
 -        final Filter<Feature> id = factory.resourceId("123");
 -        assertEquals(Feature.class, id.getResourceClass());
 +        final Filter<AbstractFeature> id = factory.resourceId("123");
++        assertEquals(AbstractFeature.class, id.getResourceClass());
          assertTrue (id.test(f1));
          assertTrue (id.test(f2));
          assertFalse(id.test(f3));
@@@ -101,6 -104,7 +102,7 @@@
                  factory.resourceId("abc"),
                  factory.resourceId("123"));
  
 -        assertEquals(Feature.class, id.getResourceClass());
++        assertEquals(AbstractFeature.class, id.getResourceClass());
          assertTrue (id.test(f1));
          assertTrue (id.test(f2));
          assertFalse(id.test(f3));
diff --cc core/sis-feature/src/test/java/org/apache/sis/filter/LeafExpressionTest.java
index f380ac0026,c1b80108a7..84976cde44
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/LeafExpressionTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/LeafExpressionTest.java
@@@ -89,9 -90,10 +89,10 @@@ public final class LeafExpressionTest e
      public void testReferenceEvaluation() {
          final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
          ftb.addAttribute(String.class).setName("some_property");
 -        final Feature f = ftb.setName("Test").build().newInstance();
 +        final AbstractFeature f = ftb.setName("Test").build().newInstance();
  
 -        ValueReference<Feature,?> ref = factory.property("some_property");
 -        assertEquals(Feature.class, ref.getResourceClass());
 +        Expression<AbstractFeature,?> ref = factory.property("some_property");
++        assertEquals(AbstractFeature.class, ref.getResourceClass());
          assertNull(ref.apply(f));
          assertNull(ref.apply(null));
  
diff --cc core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFilterTest.java
index 729fbfe24f,5111178236..6cadd74277
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFilterTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/LogicalFilterTest.java
@@@ -103,12 -108,12 +104,12 @@@ public final class LogicalFilterTest ex
       * @param  anyArity  the function creating a logical operator from an arbitrary number of operands.
       * @param  expected  expected evaluation result.
       */
-     private void create(final BiFunction<Filter<? super AbstractFeature>, Filter<? super AbstractFeature>, Filter<AbstractFeature>> binary,
-                         final Function<Collection<Filter<? super AbstractFeature>>, Filter<AbstractFeature>> anyArity,
 -    private void create(final BiFunction<Filter<Feature>, Filter<Feature>, LogicalOperator<Feature>> binary,
 -                        final Function<Collection<Filter<Feature>>, LogicalOperator<Feature>> anyArity,
++    private void create(final BiFunction<Filter<AbstractFeature>, Filter<AbstractFeature>, Filter<AbstractFeature>> binary,
++                        final Function<Collection<Filter<AbstractFeature>>, Filter<AbstractFeature>> anyArity,
                          final boolean expected)
      {
 -        final Filter<Feature> f1 = factory.isNull(factory.literal("text"));
 -        final Filter<Feature> f2 = factory.isNull(factory.literal(null));
 +        final Filter<AbstractFeature> f1 = factory.isNull(factory.literal("text"));
 +        final Filter<AbstractFeature> f2 = factory.isNull(factory.literal(null));
          try {
              binary.apply(null, null);
              fail("Creation with a null operand shall raise an exception.");
@@@ -137,8 -142,10 +138,10 @@@
          assertArrayEquals(new Filter<?>[] {f1, f2}, filter.getOperands().toArray());
          assertEquals(expected, filter.test(null));
          assertSerializedEquals(filter);
- 
+         /*
+          * Same test, using the constructor accepting any number of operands.
+          */
 -        filter = anyArity.apply(List.of(f1, f2, f1));
 +        filter = (LogicalOperator<AbstractFeature>) anyArity.apply(List.of(f1, f2, f1));
          assertArrayEquals(new Filter<?>[] {f1, f2, f1}, filter.getOperands().toArray());
          assertEquals(expected, filter.test(null));
          assertSerializedEquals(filter);
@@@ -170,6 -183,16 +179,16 @@@
  
          assertFalse(factory.not(filterTrue ).test(feature));
          assertTrue (factory.not(filterFalse).test(feature));
+         /*
+          * Test the `Predicate` methods, which should be overridden by `Optimization.OnFilter`.
+          */
 -        Predicate<Feature> predicate = filterTrue.and(filterFalse);
++        Predicate<AbstractFeature> predicate = filterTrue.and(filterFalse);
+         assertInstanceOf("Predicate.and(…)", Optimization.OnFilter.class, predicate);
+         assertFalse(predicate.test(feature));
+ 
+         predicate = filterTrue.or(filterFalse);
+         assertInstanceOf("Predicate.or(…)", Optimization.OnFilter.class, predicate);
+         assertTrue(predicate.test(feature));
      }
  
      /**
diff --cc core/sis-feature/src/test/java/org/apache/sis/filter/PeriodLiteral.java
index b39ae1750d,dc7806ef25..1a110ca368
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/PeriodLiteral.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/PeriodLiteral.java
@@@ -60,7 -73,11 +60,8 @@@ final class PeriodLiteral implements Pe
      }
  
      /** Not needed for the tests. */
 -    @Override public ReferenceIdentifier getName()                           {throw new UnsupportedOperationException();}
 -    @Override public RelativePosition relativePosition(TemporalPrimitive o)  {throw new UnsupportedOperationException();}
 -    @Override public Duration         distance(TemporalGeometricPrimitive o) {throw new UnsupportedOperationException();}
 -    @Override public Duration         length()                               {throw new UnsupportedOperationException();}
 -    @Override public <N> Expression<Feature,N> toValueType(Class<N> target)  {throw new UnsupportedOperationException();}
 +    @Override public <N> Expression<AbstractFeature,N> toValueType(Class<N> target) {throw new UnsupportedOperationException();}
++    @Override public Class<AbstractFeature> getResourceClass() {return AbstractFeature.class;}
  
      /**
       * Hash code value. Used by the tests for checking the results of deserialization.
diff --cc core/sis-feature/src/test/java/org/apache/sis/filter/TemporalFilterTest.java
index e6c11e2938,ead5d8fd3e..feb0a8a78c
--- a/core/sis-feature/src/test/java/org/apache/sis/filter/TemporalFilterTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/filter/TemporalFilterTest.java
@@@ -75,7 -78,7 +75,7 @@@ public final class TemporalFilterTest e
      private void validate(final TemporalOperatorName name) {
          assertInstanceOf("Expected SIS implementation.", TemporalFilter.class, filter);
          assertEquals("name", name, filter.getOperatorType());
-         final List<Expression<? super AbstractFeature, ?>> operands = filter.getExpressions();
 -        final List<Expression<Feature,?>> operands = filter.getExpressions();
++        final List<Expression<AbstractFeature,?>> operands = filter.getExpressions();
          assertEquals(2, operands.size());
          assertSame("expression1", expression1, operands.get(0));
          assertSame("expression2", expression2, operands.get(1));
diff --cc core/sis-feature/src/test/java/org/apache/sis/internal/filter/sqlmm/RegistryTestCase.java
index 9dd8219049,2d5e60e320..7dc2374a2b
--- a/core/sis-feature/src/test/java/org/apache/sis/internal/filter/sqlmm/RegistryTestCase.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/filter/sqlmm/RegistryTestCase.java
@@@ -468,7 -470,7 +468,7 @@@ public abstract class RegistryTestCase<
          /*
           * Optimization should evaluate the point immediately.
           */
-         final Expression<? super AbstractFeature, ?> optimized = new Optimization().apply(function);
 -        final Expression<Feature,?> optimized = new Optimization().apply(function);
++        final Expression<AbstractFeature,?> optimized = new Optimization().apply(function);
          assertNotSame("Optimization should produce a new expression.", function, optimized);
          assertInstanceOf("Expected immediate expression evaluation.", Literal.class, optimized);
          assertPointEquals(((Literal) optimized).getValue(), HardCodedCRS.WGS84_LATITUDE_FIRST, 30, 10);
@@@ -487,7 -489,7 +487,7 @@@
          final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
          ftb.addAttribute(library.pointClass).setName(P_NAME).setCRS(HardCodedCRS.WGS84);
          optimization.setFeatureType(ftb.setName("Test").build());
-         final Expression<? super AbstractFeature, ?> optimized = optimization.apply(function);
 -        final Expression<Feature,?> optimized = optimization.apply(function);
++        final Expression<AbstractFeature,?> optimized = optimization.apply(function);
          assertNotSame("Optimization should produce a new expression.", function, optimized);
          /*
           * Get the second parameter, which should be a literal, and get the point coordinates.
diff --cc storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureStream.java
index ef6d66e1e0,28649cb1c4..3b7fad02e8
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureStream.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/FeatureStream.java
@@@ -55,10 -55,10 +55,10 @@@ import org.apache.sis.internal.geoapi.f
   *
   * @author  Alexis Manin (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   * @since   1.1
   */
 -final class FeatureStream extends DeferredStream<Feature> {
 +final class FeatureStream extends DeferredStream<AbstractFeature> {
      /**
       * The table which is the source of features.
       */
diff --cc storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SelectionClauseWriter.java
index e477df9d80,0d847b22c5..2c0abf6ade
--- a/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SelectionClauseWriter.java
+++ b/storage/sis-sqlstore/src/main/java/org/apache/sis/internal/sql/feature/SelectionClauseWriter.java
@@@ -59,10 -60,10 +59,10 @@@ import org.apache.sis.internal.geoapi.f
   *
   * @author  Alexis Manin (Geomatys)
   * @author  Martin Desruisseaux (Geomatys)
-  * @version 1.1
+  * @version 1.4
   * @since   1.1
   */
 -public class SelectionClauseWriter extends Visitor<Feature, SelectionClause> {
 +public class SelectionClauseWriter extends Visitor<AbstractFeature, SelectionClause> {
      /**
       * The default instance.
       */
@@@ -88,7 -89,7 +88,7 @@@
              sql.append(" AND ");         write(sql, filter.getUpperBoundary());
          });
          setNullAndNilHandlers((filter, sql) -> {
-             final List<Expression<? super AbstractFeature, ?>> expressions = filter.getExpressions();
 -            final List<Expression<Feature, ?>> expressions = filter.getExpressions();
++            final List<Expression<AbstractFeature, ?>> expressions = filter.getExpressions();
              if (expressions.size() == 1) {
                  write(sql, expressions.get(0));
                  sql.append(" IS NULL");
@@@ -246,9 -243,8 +242,8 @@@
       * @param  expression  the expression for which to execute an action based on its type.
       * @return value of {@link SelectionClause#isInvalid} flag, for allowing caller to short-circuit.
       */
-     @SuppressWarnings("unchecked")
-     private boolean write(final SelectionClause sql, final Expression<? super AbstractFeature, ?> expression) {
-         visit((Expression<AbstractFeature, ?>) expression, sql);
 -    private boolean write(final SelectionClause sql, final Expression<Feature, ?> expression) {
++    private boolean write(final SelectionClause sql, final Expression<AbstractFeature, ?> expression) {
+         visit(expression, sql);
          return sql.isInvalid;
      }
  
@@@ -272,7 -268,7 +267,7 @@@
       * @param separator    the separator to insert between expression.
       * @param binary       whether the list of expressions shall contain exactly 2 elements.
       */
-     private void writeParameters(final SelectionClause sql, final List<Expression<? super AbstractFeature, ?>> expressions,
 -    private void writeParameters(final SelectionClause sql, final List<Expression<Feature,?>> expressions,
++    private void writeParameters(final SelectionClause sql, final List<Expression<AbstractFeature,?>> expressions,
                                   final String separator, final boolean binary)
      {
          final int n = expressions.size();
@@@ -317,9 -313,9 +312,9 @@@
          }
  
          /** Invoked when a logical filter needs to be converted to SQL clause. */
 -        @Override public void accept(final Filter<Feature> f, final SelectionClause sql) {
 -            final LogicalOperator<Feature> filter = (LogicalOperator<Feature>) f;
 -            final List<Filter<Feature>> operands = filter.getOperands();
 +        @Override public void accept(final Filter<AbstractFeature> f, final SelectionClause sql) {
-             final LogicalOperator<AbstractFeature> filter = (LogicalOperator<AbstractFeature>) f;
-             final List<Filter<? super AbstractFeature>> operands = filter.getOperands();
++            final var filter = (LogicalOperator<AbstractFeature>) f;
++            final List<Filter<AbstractFeature>> operands = filter.getOperands();
              final int n = operands.size();
              if (unary ? (n != 1) : (n == 0)) {
                  sql.invalidate();
diff --cc storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/SelectionClauseWriterTest.java
index bd404e202f,d89eb696de..25e04e0079
--- a/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/SelectionClauseWriterTest.java
+++ b/storage/sis-sqlstore/src/test/java/org/apache/sis/internal/sql/feature/SelectionClauseWriterTest.java
@@@ -140,7 -142,7 +140,7 @@@ public final class SelectionClauseWrite
       * Formats the given filter as a SQL {@code WHERE} statement body
       * and verifies that the result is equal to the expected string.
       */
-     private void verifySQL(final Filter<? super AbstractFeature> filter, final String expected) {
 -    private void verifySQL(final Filter<Feature> filter, final String expected) {
++    private void verifySQL(final Filter<AbstractFeature> filter, final String expected) {
          final SelectionClause sql = new SelectionClause(table);
          assertTrue(sql.tryAppend(SelectionClauseWriter.DEFAULT, filter));
          assertEquals(expected, sql.toString());
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
index 30be91a062,0459d9af4f..a95ce19519
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureQuery.java
@@@ -112,7 -116,7 +112,7 @@@ public class FeatureQuery extends Quer
       * @see #setSelection(Filter)
       */
      @SuppressWarnings("serial")                 // Most SIS implementations are serializable.
-     private Filter<? super AbstractFeature> selection;
 -    private Filter<Feature> selection;
++    private Filter<AbstractFeature> selection;
  
      /**
       * The number of feature instances to skip from the beginning.
@@@ -193,12 -197,12 +193,12 @@@
       * @throws IllegalArgumentException if a property is duplicated.
       */
      @SafeVarargs
-     public final void setProjection(final Expression<? super AbstractFeature, ?>... properties) {
 -    public final void setProjection(final Expression<Feature, ?>... properties) {
++    public final void setProjection(final Expression<AbstractFeature, ?>... properties) {
          NamedExpression[] wrappers = null;
          if (properties != null) {
              wrappers = new NamedExpression[properties.length];
              for (int i=0; i<wrappers.length; i++) {
-                 final Expression<? super AbstractFeature, ?> e = properties[i];
 -                final Expression<Feature, ?> e = properties[i];
++                final Expression<AbstractFeature, ?> e = properties[i];
                  ArgumentChecks.ensureNonNullElement("properties", i, e);
                  wrappers[i] = new NamedExpression(e);
              }
@@@ -259,9 -263,9 +259,9 @@@
       */
      @Override
      public void setSelection(final Envelope domain) {
-         Filter<? super AbstractFeature> filter = null;
 -        Filter<Feature> filter = null;
++        Filter<AbstractFeature> filter = null;
          if (domain != null) {
 -            final FilterFactory<Feature,Object,?> ff = DefaultFilterFactory.forFeatures();
 +            final DefaultFilterFactory<AbstractFeature,Object,?> ff = DefaultFilterFactory.forFeatures();
              filter = ff.bbox(ff.property(AttributeConvention.GEOMETRY), domain);
          }
          setSelection(filter);
@@@ -274,7 -278,7 +274,7 @@@
       *
       * @param  selection  the filter, or {@code null} if none.
       */
-     public void setSelection(final Filter<? super AbstractFeature> selection) {
 -    public void setSelection(final Filter<Feature> selection) {
++    public void setSelection(final Filter<AbstractFeature> selection) {
          this.selection = selection;
      }
  
@@@ -285,7 -289,7 +285,7 @@@
       *
       * @return the filter, or {@code null} if none.
       */
-     public Filter<? super AbstractFeature> getSelection() {
 -    public Filter<Feature> getSelection() {
++    public Filter<AbstractFeature> getSelection() {
          return selection;
      }
  
@@@ -485,7 -483,7 +485,7 @@@
           * Never {@code null}.
           */
          @SuppressWarnings("serial")
-         public final Expression<? super AbstractFeature, ?> expression;
 -        public final Expression<Feature,?> expression;
++        public final Expression<AbstractFeature,?> expression;
  
          /**
           * The name to assign to the expression result, or {@code null} if unspecified.
@@@ -508,7 -506,7 +508,7 @@@
           *
           * @param expression  the literal, value reference or expression to be retrieved by a {@code Query}.
           */
-         public NamedExpression(final Expression<? super AbstractFeature, ?> expression) {
 -        public NamedExpression(final Expression<Feature,?> expression) {
++        public NamedExpression(final Expression<AbstractFeature,?> expression) {
              this(expression, (GenericName) null);
          }
  
@@@ -518,7 -516,7 +518,7 @@@
           * @param expression  the literal, value reference or expression to be retrieved by a {@code Query}.
           * @param alias       the name to assign to the expression result, or {@code null} if unspecified.
           */
-         public NamedExpression(final Expression<? super AbstractFeature, ?> expression, final GenericName alias) {
 -        public NamedExpression(final Expression<Feature,?> expression, final GenericName alias) {
++        public NamedExpression(final Expression<AbstractFeature,?> expression, final GenericName alias) {
              this(expression, alias, ProjectionType.STORED);
          }
  
@@@ -529,7 -527,7 +529,7 @@@
           * @param expression  the literal, value reference or expression to be retrieved by a {@code Query}.
           * @param alias       the name to assign to the expression result, or {@code null} if unspecified.
           */
-         public NamedExpression(final Expression<? super AbstractFeature, ?> expression, final String alias) {
 -        public NamedExpression(final Expression<Feature,?> expression, final String alias) {
++        public NamedExpression(final Expression<AbstractFeature,?> expression, final String alias) {
              ArgumentChecks.ensureNonNull("expression", expression);
              this.expression = expression;
              this.alias = (alias != null) ? Names.createLocalName(null, null, alias) : null;
@@@ -545,7 -543,7 +545,7 @@@
           *
           * @since 1.4
           */
-         public NamedExpression(final Expression<? super AbstractFeature, ?> expression, final GenericName alias, ProjectionType type) {
 -        public NamedExpression(final Expression<Feature,?> expression, final GenericName alias, ProjectionType type) {
++        public NamedExpression(final Expression<AbstractFeature,?> expression, final GenericName alias, ProjectionType type) {
              ArgumentChecks.ensureNonNull("expression", expression);
              ArgumentChecks.ensureNonNull("type", type);
              this.expression = expression;
@@@ -689,7 -687,7 +689,7 @@@
               * For each property, get the expected type (mandatory) and its name (optional).
               * A default name will be computed if no alias were explicitly given by user.
               */
-             final Expression<? super AbstractFeature,?> expression = item.expression;
 -            final Expression<Feature,?> expression = item.expression;
++            final Expression<AbstractFeature,?> expression = item.expression;
              final FeatureExpression<?,?> fex = FeatureExpression.castOrCopy(expression);
              final PropertyTypeBuilder resultType;
              if (fex == null || (resultType = fex.expectedType(valueType, ftb)) == null) {
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java
index 71bc463b1b,920c859c43..5a1b87e95d
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/FeatureSubset.java
@@@ -110,7 -110,7 +110,7 @@@ final class FeatureSubset extends Abstr
          /*
           * Apply filter.
           */
-         final Filter<? super AbstractFeature> selection = query.getSelection();
 -        final Filter<Feature> selection = query.getSelection();
++        final Filter<AbstractFeature> selection = query.getSelection();
          if (selection != null && !selection.equals(Filter.include())) {
              stream = stream.filter(selection);
          }
@@@ -142,7 -142,7 +142,7 @@@
          final FeatureQuery.NamedExpression[] projection = query.getProjection();
          if (projection != null) {
              @SuppressWarnings({"unchecked", "rawtypes"})
-             final Expression<? super AbstractFeature, ?>[] expressions = new Expression[projection.length];
 -            final Expression<Feature,?>[] expressions = new Expression[projection.length];
++            final Expression<AbstractFeature,?>[] expressions = new Expression[projection.length];
              for (int i=0; i<expressions.length; i++) {
                  expressions[i] = projection[i].expression;
              }
diff --cc storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/JoinFeatureSet.java
index 61931e80bf,c4a98d419b..ea0c32ac16
--- a/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/JoinFeatureSet.java
+++ b/storage/sis-storage/src/main/java/org/apache/sis/storage/aggregate/JoinFeatureSet.java
@@@ -174,7 -176,7 +174,7 @@@ public class JoinFeatureSet extends Agg
       * This condition specifies also if the comparison is {@linkplain BinaryComparisonOperator#isMatchingCase() case
       * sensitive} and {@linkplain BinaryComparisonOperator#getMatchAction() how to compare multi-values}.
       */
-     public final BinaryComparisonOperator<? super AbstractFeature> condition;
 -    public final BinaryComparisonOperator<Feature> condition;
++    public final BinaryComparisonOperator<AbstractFeature> condition;
  
      /**
       * The factory to use for creating {@code Query} expressions for retrieving subsets of feature sets.
@@@ -206,7 -208,7 +206,7 @@@
      public JoinFeatureSet(final Resource parent,
                            final FeatureSet left,  String leftAlias,
                            final FeatureSet right, String rightAlias,
-                           final Type joinType, final BinaryComparisonOperator<? super AbstractFeature> condition,
 -                          final Type joinType, final BinaryComparisonOperator<Feature> condition,
++                          final Type joinType, final BinaryComparisonOperator<AbstractFeature> condition,
                            Map<String,?> featureInfo)
              throws DataStoreException
      {
@@@ -462,7 -464,7 +462,7 @@@
           * The filtering condition is determined by the current {@link #mainFeature}.
           */
          private void createFilteredIterator() {
-             final Expression<? super AbstractFeature, ?> expression1, expression2;
 -            final Expression<Feature,?> expression1, expression2;
++            final Expression<AbstractFeature,?> expression1, expression2;
              final FeatureSet filteredSet;
              if (swapSides) {
                  expression1 = condition.getOperand2();
@@@ -474,9 -476,10 +474,9 @@@
                  filteredSet = right;
              }
              final Object mainValue = expression1.apply(mainFeature);
-             final Filter<? super AbstractFeature> filter;
 -            final Filter<Feature> filter;
++            final Filter<AbstractFeature> filter;
              if (mainValue != null) {
 -                filter = factory.equal(expression2, factory.literal(mainValue),
 -                            condition.isMatchingCase(), condition.getMatchAction());
 +                filter = factory.equal(expression2, factory.literal(mainValue));
              } else {
                  filter = factory.isNull(expression2);
              }
diff --cc storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
index 91886d5b03,b4aa6f3786..04073e0a50
--- a/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
+++ b/storage/sis-storage/src/test/java/org/apache/sis/storage/FeatureQueryTest.java
@@@ -272,16 -291,16 +272,16 @@@ public final class FeatureQueryTest ext
                              new FeatureQuery.NamedExpression(ff.property("/*/unknown"), "unexpected"));
  
          // Check result type.
 -        final Feature instance = executeAndGetFirst();
 -        final FeatureType resultType = instance.getType();
 +        final AbstractFeature instance = executeAndGetFirst();
 +        final DefaultFeatureType resultType = instance.getType();
          assertEquals("Test", resultType.getName().toString());
          assertEquals(2, resultType.getProperties(true).size());
 -        final PropertyType pt1 = resultType.getProperty("value1");
 -        final PropertyType pt2 = resultType.getProperty("unexpected");
 -        assertTrue(pt1 instanceof AttributeType<?>);
 -        assertTrue(pt2 instanceof AttributeType<?>);
 -        assertEquals(Integer.class, ((AttributeType<?>) pt1).getValueClass());
 -        assertEquals(Object.class,  ((AttributeType<?>) pt2).getValueClass());
 +        final AbstractIdentifiedType pt1 = resultType.getProperty("value1");
 +        final AbstractIdentifiedType pt2 = resultType.getProperty("unexpected");
-         assertTrue(pt1 instanceof DefaultAttributeType);
-         assertTrue(pt2 instanceof DefaultAttributeType);
-         assertEquals(Integer.class, ((DefaultAttributeType) pt1).getValueClass());
-         assertEquals(Object.class,  ((DefaultAttributeType) pt2).getValueClass());
++        assertTrue(pt1 instanceof DefaultAttributeType<?>);
++        assertTrue(pt2 instanceof DefaultAttributeType<?>);
++        assertEquals(Integer.class, ((DefaultAttributeType<?>) pt1).getValueClass());
++        assertEquals(Object.class,  ((DefaultAttributeType<?>) pt2).getValueClass());
  
          // Check feature property values.
          assertEquals(3,    instance.getPropertyValue("value1"));
@@@ -308,7 -327,7 +308,7 @@@
      /**
       * Shortcut for creating expression for a projection computed on-the-fly.
       */
-     private static FeatureQuery.NamedExpression virtualProjection(final Expression<? super AbstractFeature, ?> expression, final String alias) {
 -    private static FeatureQuery.NamedExpression virtualProjection(final Expression<Feature, ?> expression, final String alias) {
++    private static FeatureQuery.NamedExpression virtualProjection(final Expression<AbstractFeature, ?> expression, final String alias) {
          return new FeatureQuery.NamedExpression(expression, Names.createLocalName(null, null, alias), FeatureQuery.ProjectionType.VIRTUAL);
      }
  
@@@ -326,21 -345,23 +326,23 @@@
                  virtualProjection(ff.literal("a literal"), "computed"));
  
          // Check result type.
 -        final Feature instance = executeAndGetFirst();
 -        final FeatureType resultType = instance.getType();
 +        final AbstractFeature instance = executeAndGetFirst();
 +        final DefaultFeatureType resultType = instance.getType();
          assertEquals("Test", resultType.getName().toString());
          assertEquals(3, resultType.getProperties(true).size());
 -        final PropertyType pt1 = resultType.getProperty("value1");
 -        final PropertyType pt2 = resultType.getProperty("renamed1");
 -        final PropertyType pt3 = resultType.getProperty("computed");
 -        assertTrue(pt1 instanceof AttributeType<?>);
 -        assertTrue(pt2 instanceof Operation);
 -        assertTrue(pt3 instanceof Operation);
 -        final IdentifiedType result2 = ((Operation) pt2).getResult();
 -        final IdentifiedType result3 = ((Operation) pt3).getResult();
 -        assertEquals(Integer.class, ((AttributeType<?>) pt1).getValueClass());
 -        assertTrue(result2 instanceof AttributeType<?>);
 -        assertTrue(result3 instanceof AttributeType<?>);
 -        assertEquals(Integer.class, ((AttributeType<?>) result2).getValueClass());
 -        assertEquals(String.class,  ((AttributeType<?>) result3).getValueClass());
 +        final AbstractIdentifiedType pt1 = resultType.getProperty("value1");
 +        final AbstractIdentifiedType pt2 = resultType.getProperty("renamed1");
 +        final AbstractIdentifiedType pt3 = resultType.getProperty("computed");
-         assertTrue(pt1 instanceof DefaultAttributeType);
++        assertTrue(pt1 instanceof DefaultAttributeType<?>);
 +        assertTrue(pt2 instanceof AbstractOperation);
 +        assertTrue(pt3 instanceof AbstractOperation);
-         assertEquals(Integer.class, ((DefaultAttributeType) pt1).getValueClass());
-         assertTrue(((AbstractOperation) pt2).getResult() instanceof DefaultAttributeType);
-         assertTrue(((AbstractOperation) pt3).getResult() instanceof DefaultAttributeType);
-         assertEquals(Integer.class, ((DefaultAttributeType)((AbstractOperation) pt2).getResult()).getValueClass());
-         assertEquals(String.class,  ((DefaultAttributeType)((AbstractOperation) pt3).getResult()).getValueClass());
++        final AbstractIdentifiedType result2 = ((AbstractOperation) pt2).getResult();
++        final AbstractIdentifiedType result3 = ((AbstractOperation) pt3).getResult();
++        assertEquals(Integer.class, ((DefaultAttributeType<?>) pt1).getValueClass());
++        assertTrue(result2 instanceof DefaultAttributeType<?>);
++        assertTrue(result3 instanceof DefaultAttributeType<?>);
++        assertEquals(Integer.class, ((DefaultAttributeType<?>) result2).getValueClass());
++        assertEquals(String.class,  ((DefaultAttributeType<?>) result3).getValueClass());
  
          // Check feature instance.
          assertEquals(3, instance.getPropertyValue("value1"));