Repository: jpype-project/jpype (language: python)

Issue:
Python lambda to FunctionalInterface conversion is too strict
Note the following line in the documentation for `FunctionalInterface` seen [here](https://docs.oracle.com/javase/8/docs/api/java/lang/FunctionalInterface.html):

"However, the compiler will treat any interface meeting the definition of a functional interface as a functional interface regardless of whether or not a FunctionalInterface annotation is present on the interface declaration."

Unfortunately, jpype does **not** allow conversion from a lambda to a functional interface if the Java interface does not have the `@FunctionInterface` annotation. However, this should not be the case and any functional interface should be convertable.

The below java and python code snippits provide a simple demonstration of the problem.

```java
package tmp;

public class App {
    
    public static void printNonAnnotated(NonAnnotatedFunctionInterface messageGetter) {
        System.out.println(messageGetter.getMessage());
    }
    
    public static void printAnnotated(AnnotatedFunctionInterface messageGetter) {
        printNonAnnotated(messageGetter);
    }
    
    public static void main(String[] args) {
        printNonAnnotated(() -> "Hello from non annotated interface");
        printAnnotated(() -> "Hello from annotated interface");
    }
    
    public static interface NonAnnotatedFunctionInterface {
        
        public String getMessage();
    }
    
    @FunctionalInterface
    public static interface AnnotatedFunctionInterface extends NonAnnotatedFunctionInterface {
    }
}
```

```python
import jpype
from jpype.imports import *

if __name__ == '__main__':
    jpype.startJVM(classpath="tmp.jar")
    from tmp import App
    App.main([])
    App.printAnnotated(lambda : "Hello from Python annotated interface")
    App.printNonAnnotated(lambda : "Hello from Python non annotated interface")
```

Running the above python code results in the following output:

```
Hello from non annotated interface
Hello from annotated interface
Hello from Python annotated interface
Traceback (most recent call last):
  File "C:\Users\astre\Documents\tmp\main.py", line 9, in <module>
    App.printNonAnnotated(lambda : "Hello from Python non annotated interface")
TypeError: No matching overloads found for *static* tmp.App.printNonAnnotated(function), options are:
        public static void tmp.App.printNonAnnotated(tmp.App$NonAnnotatedFunctionInterface)
```


Discussion / hints:
I can try to figure out a pattern for identifying a SAM (single abstract method), but there isn't such a thing in the Java API so the only way to know currently is to check for the FunctionInterface annotation or to fully inspect all methods in the class at runtime.   It is really the issue with Java that the compiler doesn't automatically annotation rather than a bug in JPype.   As it currently stands we only identify a SAM by annotation.
> I can try to figure out a pattern for identifying a SAM (single abstract method), but there isn't such a thing in the Java API so the only way to know currently is to check for the FunctionInterface annotation or to fully inspect all methods in the class at runtime. It is really the issue with Java that the compiler doesn't automatically annotation rather than a bug in JPype. As it currently stands we only identify a SAM by annotation.

I'm not familiar with jni, but I would expect equivalent functions to be available for the below Java code. This was the most trivial solution I could come up with which would short circuit on non functional interfaces. Worst case is for a function interface with numerous static and default methods.

```java
public static Method getFunctionalInterfaceMethod(Class<?> cls) {
    // Not sure how someone would pass in an annotation, but may as well check to be oo the safe side
    if (!cls.isInterface() || cls.isAnnotation()) {
        return null;
    }
    Method result = null;
    // Class.getMethods() includes inherited methods and interfaces can only have public methods
    for (Method m : cls.getMethods()) {
        if (Modifier.isAbstract(m.getModifiers())) {
            if (result != null) {
                return null;
            }
            result = m;
        }
    }
    return result;
}
```
The good news is that the code for this is pure Java.  The bad news is I am not sure off the side effects that are likely to happen...

The determination is made in ``native/java/org/jpype/manager/TypeManager.java`` on line 475

```
    if (this.functionalAnnotation != null
            && cls.getAnnotation(this.functionalAnnotation) != null)
      modifiers |= ModifierCode.FUNCTIONAL.value | ModifierCode.SPECIAL.value;
```

So that just triggers the logic to start the process.   The next part has to figure out the name of the SAM.

This is in ``native/org/jpype/JPypeContext.java``
```
  /**
   * Utility to probe functional interfaces.
   *
   * @param cls
   * @return
   */
  public String getFunctional(Class cls)
  {
    // If we don't find it to be a functional interface, then we won't return
    // the SAM.
    if (cls.getDeclaredAnnotation(FunctionalInterface.class) == null)
      return null;
    for (Method m : cls.getMethods())
    {
      if (Modifier.isAbstract(m.getModifiers()))
      {
        // This is a very odd construct.  Java allows for java.lang.Object
        // methods to declared in FunctionalInterfaces and they don't count
        // towards the single abstract method. So we have to probe the class
        // until we find something that fails.
        try
        {
          Object.class.getMethod(m.getName(), m.getParameterTypes());
        } catch (NoSuchMethodException | SecurityException ex)
        {
          return m.getName();
        }
      }
    }
    return null;
  }
```

This method shouldn't be used except on a SAM, because it will generate an exception for each method in the class (which is slow and costly).


You will need to test your code on a wide variety of cases, such as default methods, inherited interfaces that are SAM or not (from parent or from child).   

```
public class Test
{
  @FunctionalInterface
  public interface A
  {
    void f(); // SAM 
    default void g() {}
  }
  
  @FunctionalInterface
  public interface B extends A
  {
    // f is still the SAM
    default void h() {}
  }
  
  @FunctionalInterface
  public interface C extends A
  {
    void h();  // NOT SAM!
  }
  
  @FunctionalInterface
  public interface D extends Serializable
  {
    void h();  // SAM
  } 
}
```
```
        // This is a very odd construct.  Java allows for java.lang.Object
        // methods to declared in FunctionalInterfaces and they don't count
        // towards the single abstract method. So we have to probe the class
        // until we find something that fails.
```

That's an annoying quirk. I've played around with this a bit more and think I've narrowed down exactly what is allowed and what isn't. I'm a bit tired so I'll need to re-look at this with a fresh set of eyes, but I may be able to do away with the `Object.class.getMethod` call. I'll probably open a pull request in a few days or over the weekend with something.
All good.  I look forward to the PR.

As I mentioned at the start, it isn't that we can't support the full Java specification, it is simply that since Java lacks methods to determine what qualifies as a SAM, we end up having to write something ourselves and then try to find all the edge cases.   The FunctionalInterface only served my needs but if we can hit the full specification that is great.



Fix patch:
diff --git a/doc/CHANGELOG.rst b/doc/CHANGELOG.rst
index 004106370..123970f5c 100644
--- a/doc/CHANGELOG.rst
+++ b/doc/CHANGELOG.rst
@@ -9,6 +9,8 @@ Latest Changes:
   - Fixed crash when calling subscript on JArray.
 
   - Fixed direct byte buffers not reporting nbytes correctly when cast to memoryview.
+  - Expand the defintion for Functional interface to include classes without 
+    FunctionInterface annotation.
 
 - **1.4.1 - 2022-10-26**
   
diff --git a/native/common/jp_context.cpp b/native/common/jp_context.cpp
index 7e1184a06..d5c2477ae 100644
--- a/native/common/jp_context.cpp
+++ b/native/common/jp_context.cpp
@@ -287,6 +287,12 @@ void JPContext::initializeResources(JNIEnv* env, bool interrupt)
 		JPPyObject origin = JPPyObject::call(PyObject_GetAttrString(jpype.get(), "origin"));
 		val[2].l = frame.fromStringUTF8(JPPyString::asStringUTF8(origin.get()));
 	}
+
+	// Required before launch
+	m_Context_GetFunctionalID = frame.GetStaticMethodID(contextClass,
+			"getFunctional",
+			"(Ljava/lang/Class;)Ljava/lang/String;");
+
 	m_JavaContext = JPObjectRef(frame, frame.CallStaticObjectMethodA(contextClass, startMethod, val));
 
 	// Post launch
@@ -309,10 +315,6 @@ void JPContext::initializeResources(JNIEnv* env, bool interrupt)
 			"assemble",
 			"([ILjava/lang/Object;)Ljava/lang/Object;");
 
-	m_Context_GetFunctionalID = frame.GetMethodID(contextClass,
-			"getFunctional",
-			"(Ljava/lang/Class;)Ljava/lang/String;");
-
 	m_Context_CreateExceptionID = frame.GetMethodID(contextClass, "createException",
 			"(JJ)Ljava/lang/Exception;");
 	m_Context_GetExcClassID = frame.GetMethodID(contextClass, "getExcClass",
diff --git a/native/common/jp_javaframe.cpp b/native/common/jp_javaframe.cpp
index 925042930..d05b0480a 100644
--- a/native/common/jp_javaframe.cpp
+++ b/native/common/jp_javaframe.cpp
@@ -1170,8 +1170,8 @@ string JPJavaFrame::getFunctional(jclass c)
 {
 	jvalue v;
 	v.l = (jobject) c;
-	return toStringUTF8((jstring) CallObjectMethodA(
-			m_Context->m_JavaContext.get(),
+	return toStringUTF8((jstring) CallStaticObjectMethodA(
+			m_Context->m_ContextClass.get(),
 			m_Context->m_Context_GetFunctionalID, &v));
 }
 
diff --git a/native/java/org/jpype/JPypeContext.java b/native/java/org/jpype/JPypeContext.java
index bae78f6ca..c34e2b113 100644
--- a/native/java/org/jpype/JPypeContext.java
+++ b/native/java/org/jpype/JPypeContext.java
@@ -566,30 +566,10 @@ public JPypePackage getPackage(String s)
    * @param cls
    * @return
    */
-  public String getFunctional(Class cls)
+  public static String getFunctional(Class cls)
   {
-    // If we don't find it to be a functional interface, then we won't return
-    // the SAM.
-    if (cls.getDeclaredAnnotation(FunctionalInterface.class) == null)
-      return null;
-    for (Method m : cls.getMethods())
-    {
-      if (Modifier.isAbstract(m.getModifiers()))
-      {
-        // This is a very odd construct.  Java allows for java.lang.Object
-        // methods to declared in FunctionalInterfaces and they don't count
-        // towards the single abstract method. So we have to probe the class
-        // until we find something that fails.
-        try
-        {
-          Object.class.getMethod(m.getName(), m.getParameterTypes());
-        } catch (NoSuchMethodException | SecurityException ex)
-        {
-          return m.getName();
-        }
-      }
-    }
-    return null;
+    Method m = JPypeUtilities.getFunctionalInterfaceMethod(cls);
+    return m != null ? m.getName() : null;
   }
 
   /**
diff --git a/native/java/org/jpype/JPypeUtilities.java b/native/java/org/jpype/JPypeUtilities.java
index 3939eefe3..789db6ddf 100644
--- a/native/java/org/jpype/JPypeUtilities.java
+++ b/native/java/org/jpype/JPypeUtilities.java
@@ -1,11 +1,47 @@
 package org.jpype;
 
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandleProxies;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
 import java.net.URISyntaxException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.function.Predicate;
 
 public class JPypeUtilities
 {
+  
+  // a functional interface can only re-declare a public non-final method from Object
+  // this should end up being an array of equals, hashCode and toString
+  private static final Method[] OBJECT_METHODS =
+    Arrays.stream(Object.class.getMethods())
+      .filter(m -> !Modifier.isFinal(m.getModifiers()))
+      .toArray(Method[]::new);
+  
+  private static final Predicate<Class> isSealed;
+
+  static
+  {
+    Predicate<Class> result = null;
+    try
+    {
+      Method m = Class.class.getMethod("isSealed");
+      MethodHandle handle = MethodHandles.publicLookup().unreflect(m);
+      result = MethodHandleProxies.asInterfaceInstance(Predicate.class, handle);
+    } catch (IllegalAccessException e)
+    {
+      // it's a public method so this should never occur
+      throw new IllegalAccessError(e.getMessage());
+    } catch (NoSuchMethodException e)
+    {
+      // if isSealed doesn't exist then neither do sealed classes
+      result = c -> false;
+    }
+    isSealed = result;
+  }
 
   public static Path getJarPath(Class c)
   {
@@ -19,4 +55,56 @@ public static Path getJarPath(Class c)
     }
   }
 
+  public static Method getFunctionalInterfaceMethod(Class cls)
+  {
+    if (!cls.isInterface() || cls.isAnnotation() || isSealed.test(cls))
+      return null;
+
+    Method result = null;
+    for (Method m : cls.getMethods())
+    {
+      if (Modifier.isAbstract(m.getModifiers()))
+      {
+        if (isObjectMethodOverride(m))
+          continue;
+        
+        if (result != null && !equals(m, result))
+          return null;
+
+        if (result == null || cls.equals(m.getDeclaringClass()))
+          result = m;
+      }
+    }
+    return result;
+  }
+
+  private static boolean isObjectMethodOverride(Method m)
+  {
+    for (Method objectMethod : OBJECT_METHODS)
+    {
+      if (equals(m, objectMethod))
+        return true;
+    }
+    return false;
+  }
+  
+  private static boolean equals(Method a, Method b)
+  {
+    // this should be the fastest possible short circuit
+    if (a.getParameterCount() != b.getParameterCount())
+      return false;
+
+    if (!a.getName().equals(b.getName()))
+      return false;
+
+    // if the return types are different it wouldn't compile
+    // parameters must be exactly the same and may not be an extended class
+    if (!Arrays.equals(a.getParameterTypes(), b.getParameterTypes()))
+      return false;
+
+    // if declared exceptions were different it wouldn't compile
+    // if it did compile then it is an override
+    return true;
+  }
+  
 }
diff --git a/native/java/org/jpype/manager/ClassDescriptor.java b/native/java/org/jpype/manager/ClassDescriptor.java
index aefe1aff8..b03baf784 100644
--- a/native/java/org/jpype/manager/ClassDescriptor.java
+++ b/native/java/org/jpype/manager/ClassDescriptor.java
@@ -46,14 +46,18 @@ public class ClassDescriptor
   public int methodCounter = 0;
   public long[] fields;
   public long anonymous;
-  public int functional_interface_parameter_count = -1; // -1 = unset; -2 = not a functional interface
+  public int functional_interface_parameter_count; 
 
-  ClassDescriptor(Class cls, long classPtr)
+  ClassDescriptor(Class cls, long classPtr, Method method)
   {
     this.cls = cls;
     this.classPtr = classPtr;
     if (this.classPtr == 0)
       throw new NullPointerException("Class pointer is null for " + cls);
+    if (method != null)
+      functional_interface_parameter_count = method.getParameterCount();
+    else
+      functional_interface_parameter_count = -1;
   }
 
   long getMethod(Method requestedMethod)
diff --git a/native/java/org/jpype/manager/TypeManager.java b/native/java/org/jpype/manager/TypeManager.java
index 3cdd9ea9d..491a53834 100644
--- a/native/java/org/jpype/manager/TypeManager.java
+++ b/native/java/org/jpype/manager/TypeManager.java
@@ -33,6 +33,7 @@
 import java.util.List;
 import java.util.TreeSet;
 import org.jpype.JPypeContext;
+import org.jpype.JPypeUtilities;
 import org.jpype.proxy.JPypeProxy;
 
 /**
@@ -48,7 +49,6 @@ public class TypeManager
   public TypeFactory typeFactory = null;
   public TypeAudit audit = null;
   private ClassDescriptor java_lang_Object;
-  public Class<? extends Annotation> functionalAnnotation = null;
   // For reasons that are less than clear, this object cannot be created
   // during shutdown
   private Destroyer destroyer = new Destroyer();
@@ -73,15 +73,6 @@ public synchronized void init()
       isStarted = true;
       isShutdown = false;
 
-      try
-      {
-        this.functionalAnnotation = Class.forName("java.lang.FunctionalInterface")
-                .asSubclass(Annotation.class);
-      } catch (ClassNotFoundException ex)
-      {
-        // It is okay if we don't find this
-      }
-
       // Create the required minimum classes
       this.java_lang_Object = createClass(Object.class, true);
 
@@ -306,26 +297,10 @@ public synchronized void populateMethod(long wrapper, Executable method)
    * @param interfaceClass The class of the interface
    * @return the number of arguments the only unimplemented method of the interface accept.
    */
-  public int interfaceParameterCount(Class<?> interfaceClass) {
+  public int interfaceParameterCount(Class<?> interfaceClass)
+  {
     ClassDescriptor classDescriptor = classMap.get(interfaceClass);
-    if (classDescriptor.functional_interface_parameter_count != -1) {
-      return classDescriptor.functional_interface_parameter_count;
-    }
-    int abstractMethodParameterCount = -1;
-    for (Method method : interfaceClass.getMethods()) {
-      if (Modifier.isAbstract(method.getModifiers())) {
-        if (abstractMethodParameterCount != -1) {
-          classDescriptor.functional_interface_parameter_count = -2;
-          return -2;
-        }
-        abstractMethodParameterCount = method.getParameterCount();
-      }
-    }
-    if (abstractMethodParameterCount == -1) {
-      abstractMethodParameterCount = -2;
-    }
-    classDescriptor.functional_interface_parameter_count = abstractMethodParameterCount;
-    return abstractMethodParameterCount;
+    return classDescriptor.functional_interface_parameter_count;
   }
 
   /**
@@ -472,8 +447,10 @@ private ClassDescriptor createOrdinaryClass(Class<?> cls, boolean special, boole
       modifiers |= ModifierCode.COMPARABLE.value;
     if (Buffer.class.isAssignableFrom(cls))
       modifiers |= ModifierCode.BUFFER.value | ModifierCode.SPECIAL.value;
-    if (this.functionalAnnotation != null
-            && cls.getAnnotation(this.functionalAnnotation) != null)
+
+    // Check if is Functional class
+    Method method = JPypeUtilities.getFunctionalInterfaceMethod(cls);
+    if (method != null)
       modifiers |= ModifierCode.FUNCTIONAL.value | ModifierCode.SPECIAL.value;
 
     // FIXME watch out for anonyous and lambda here.
@@ -488,9 +465,8 @@ private ClassDescriptor createOrdinaryClass(Class<?> cls, boolean special, boole
             modifiers);
 
     // Cache the wrapper.
-    ClassDescriptor out = new ClassDescriptor(cls, classPtr);
+    ClassDescriptor out = new ClassDescriptor(cls, classPtr, method);
     this.classMap.put(cls, out);
-
     return out;
   }
 
@@ -525,7 +501,7 @@ ClassDescriptor createArrayClass(Class cls)
                     componentTypePtr,
                     modifiers);
 
-    ClassDescriptor out = new ClassDescriptor(cls, classPtr);
+    ClassDescriptor out = new ClassDescriptor(cls, classPtr, null);
     this.classMap.put(cls, out);
     return out;
   }
@@ -544,7 +520,7 @@ private void createPrimitive(String name, Class cls, Class boxed)
             cls,
             this.getClass(boxed).classPtr,
             cls.getModifiers() & 0xffff);
-    this.classMap.put(cls, new ClassDescriptor(cls, classPtr));
+    this.classMap.put(cls, new ClassDescriptor(cls, classPtr, null));
   }
 
 //</editor-fold>


Test patch:
diff --git a/test/harness/jpype/functional/Annotated.java b/test/harness/jpype/functional/Annotated.java
new file mode 100644
index 000000000..4c0a366d1
--- /dev/null
+++ b/test/harness/jpype/functional/Annotated.java
@@ -0,0 +1,22 @@
+/* ****************************************************************************
+  Licensed 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.
+
+  See NOTICE file for details.
+**************************************************************************** */
+package jpype.functional;
+
+@FunctionalInterface
+public interface Annotated
+{
+  int call(int i);
+}
diff --git a/test/harness/jpype/functional/AnnotatedWithObjectMethods.java b/test/harness/jpype/functional/AnnotatedWithObjectMethods.java
new file mode 100644
index 000000000..1201d81da
--- /dev/null
+++ b/test/harness/jpype/functional/AnnotatedWithObjectMethods.java
@@ -0,0 +1,25 @@
+/* ****************************************************************************
+  Licensed 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.
+
+  See NOTICE file for details.
+**************************************************************************** */
+package jpype.functional;
+
+@FunctionalInterface
+public interface AnnotatedWithObjectMethods
+{
+  int call(int i);
+  boolean equals(Object o);
+  int hashCode();
+  String toString();
+}
diff --git a/test/harness/jpype/functional/ExtendsFunctional.java b/test/harness/jpype/functional/ExtendsFunctional.java
new file mode 100644
index 000000000..853091a4d
--- /dev/null
+++ b/test/harness/jpype/functional/ExtendsFunctional.java
@@ -0,0 +1,20 @@
+/* ****************************************************************************
+  Licensed 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.
+
+  See NOTICE file for details.
+**************************************************************************** */
+package jpype.functional;
+
+public interface ExtendsFunctional extends NonAnnotated
+{
+}
diff --git a/test/harness/jpype/functional/ExtendsNonFunctional.java b/test/harness/jpype/functional/ExtendsNonFunctional.java
new file mode 100644
index 000000000..6b88e7365
--- /dev/null
+++ b/test/harness/jpype/functional/ExtendsNonFunctional.java
@@ -0,0 +1,21 @@
+/* ****************************************************************************
+  Licensed 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.
+
+  See NOTICE file for details.
+**************************************************************************** */
+package jpype.functional;
+
+public interface ExtendsNonFunctional extends NonAnnotated
+{
+  boolean test();
+}
diff --git a/test/harness/jpype/functional/NonAnnotated.java b/test/harness/jpype/functional/NonAnnotated.java
new file mode 100644
index 000000000..973dda077
--- /dev/null
+++ b/test/harness/jpype/functional/NonAnnotated.java
@@ -0,0 +1,21 @@
+/* ****************************************************************************
+  Licensed 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.
+
+  See NOTICE file for details.
+**************************************************************************** */
+package jpype.functional;
+
+public interface NonAnnotated
+{
+  int call(int i);
+}
diff --git a/test/harness/jpype/functional/NonAnnotatedWithObjectMethods.java b/test/harness/jpype/functional/NonAnnotatedWithObjectMethods.java
new file mode 100644
index 000000000..72b275834
--- /dev/null
+++ b/test/harness/jpype/functional/NonAnnotatedWithObjectMethods.java
@@ -0,0 +1,24 @@
+/* ****************************************************************************
+  Licensed 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.
+
+  See NOTICE file for details.
+**************************************************************************** */
+package jpype.functional;
+
+public interface NonAnnotatedWithObjectMethods
+{
+  int call(int i);
+  boolean equals(Object o);
+  int hashCode();
+  String toString();
+}
diff --git a/test/harness/jpype/functional/RedeclaresAnnotated.java b/test/harness/jpype/functional/RedeclaresAnnotated.java
new file mode 100644
index 000000000..ac75b25f6
--- /dev/null
+++ b/test/harness/jpype/functional/RedeclaresAnnotated.java
@@ -0,0 +1,21 @@
+/* ****************************************************************************
+  Licensed 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.
+
+  See NOTICE file for details.
+**************************************************************************** */
+package jpype.functional;
+
+public interface RedeclaresAnnotated extends Annotated
+{
+  int call(int i);
+}
diff --git a/test/harness/jpype/functional/RedeclaresNonAnnotated.java b/test/harness/jpype/functional/RedeclaresNonAnnotated.java
new file mode 100644
index 000000000..038cdfe70
--- /dev/null
+++ b/test/harness/jpype/functional/RedeclaresNonAnnotated.java
@@ -0,0 +1,21 @@
+/* ****************************************************************************
+  Licensed 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.
+
+  See NOTICE file for details.
+**************************************************************************** */
+package jpype.functional;
+
+public interface RedeclaresNonAnnotated extends NonAnnotated
+{
+  int call(int i);
+}
diff --git a/test/jpypetest/test_functional.py b/test/jpypetest/test_functional.py
new file mode 100644
index 000000000..1752fb511
--- /dev/null
+++ b/test/jpypetest/test_functional.py
@@ -0,0 +1,62 @@
+# *****************************************************************************
+#
+#   Licensed 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.
+#
+#   See NOTICE file for details.
+#
+# *****************************************************************************
+import jpype
+import common
+
+
+def callFunctional(cls, i):
+    fun = cls @(lambda x: x)
+    return fun.call(i)
+
+
+class FunctionalTestCase(common.JPypeTestCase):
+    
+    def _checkValidFunctional(self, name, value):
+        cls = jpype.JClass("jpype.functional." + name)
+        self.assertEqual(callFunctional(cls, value), value)
+    
+    def testAnnotated(self):
+        self._checkValidFunctional("Annotated", 0)
+
+    def testNonAnnotated(self):
+        self._checkValidFunctional("NonAnnotated", 1)
+
+    def testExtendsFunctional(self):
+        self._checkValidFunctional("ExtendsFunctional", 2)
+
+    def testExtendsNonFunctional(self):
+        with self.assertRaises(TypeError):
+            self._checkValidFunctional("ExtendsNonFunctional", 3)
+    
+    def testAnnotatedWithObjectMethods(self):
+        self._checkValidFunctional("AnnotatedWithObjectMethods", 4)
+
+    def testNonAnnotatedWithObjectMethods(self):
+        self._checkValidFunctional("NonAnnotatedWithObjectMethods", 5)
+
+    def testRedeclaresAnnotated(self):
+        self._checkValidFunctional("RedeclaresAnnotated", 6)
+
+    def testRedeclaresNonAnnotated(self):
+        self._checkValidFunctional("RedeclaresNonAnnotated", 7)
+
+    def testStaticDefaultMethods(self):
+        # Predicate has both static and default methods
+        Predicate = jpype.JClass("java.util.function.Predicate")
+        fun = Predicate @ (lambda x: True)
+        self.assertTrue(fun.test(fun))
diff --git a/test/jpypetest/test_lambdas.py b/test/jpypetest/test_lambdas.py
index 017d5f581..f11068f34 100644
--- a/test/jpypetest/test_lambdas.py
+++ b/test/jpypetest/test_lambdas.py
@@ -16,9 +16,6 @@
 #
 # *****************************************************************************
 import jpype
-import sys
-import logging
-import time
 import common