Java 8中“功能接口”的精确定义

78
最近我开始探索Java 8,但我无法完全理解“函数接口”的概念,这对于Java实现lambda表达式至关重要。虽然有一份Java lambda函数的非常全面的指南,但我卡在了定义函数接口概念的章节上。定义如下:

更准确地说,函数接口被定义为仅具有一个抽象方法的任何接口。

然后他举了例子,其中之一是Comparator接口:
public interface Comparator<T> {
    int compare(T o1, T o2);
    boolean equals(Object obj);
} 
我测试了Lambda函数可以替代Comparator参数的使用,例如:Collections.sort(list, (a, b) -> a-b),并且它可以工作。
但是在Comparator接口中,compare和equals方法都是抽象的,这意味着它有两个抽象方法。根据定义,一个接口应该只有一个抽象方法,那么为什么这里还能正常工作呢?我漏掉了什么吗?
9个回答

72

来自你链接到的同一页

接口Comparator是功能性的,因为尽管它声明了两个抽象方法,但其中一个equals具有与Object中的公共方法相对应的签名。接口总是声明与Object的公共方法相对应的抽象方法,但通常是隐式声明的。无论是隐式声明还是显式声明,这些方法都不计入数量。

我真的说不出更好的了。


1
但是,由于原始类型不是对象,因此我们无法有一个扩展任何东西的原始类型。而且,由于在Java中创建的任何类都会扩展Object,那么在其中拥有“boolean equals(Object obj);”的目的是什么呢?任何实现都将无论如何访问该方法。这纯粹是为了强调这种方法是成为某些“可比较”的实现进行修改的绝佳候选方法吗? - payne

16

另一个解释在@FunctionalInterface页面中给出:

从概念上讲,函数式接口恰好有一个抽象方法。由于默认方法有一个实现,它们不是抽象的。如果一个接口声明一个覆盖了java.lang.Object的公共方法的抽象方法,那么这也不计入接口的抽象方法计数,因为接口的任何实现都将具有来自java.lang.Object或其他地方的实现。

您可以使用@FunctionalInterface测试哪个接口是正确的函数式接口

例如:

  • 这有效

  • @FunctionalInterface
    public interface FunctionalInterf {
    
        void m();
    
        boolean equals(Object o);
    
    }
    
  • 这会生成一个错误:

  • @FunctionalInterface
    public interface FunctionalInterf {
    
        void m();
    
        boolean equals();
    
    }
    
  • 在接口FunctionalInterf中发现多个非覆盖抽象方法


但是接口应该扩展Object,以使此Override情况有效。 - Prateek Pande

3
问:但是在比较器接口中,compare()和equals()方法都是抽象的,这意味着它有两个抽象方法。那么如果定义要求接口具有恰好一个抽象方法,这怎么可能工作呢?我在这里错过了什么?
答:函数式接口可以指定由Object定义的任何公共方法(例如equals()),而不影响其"函数式接口"状态。公共Object方法被视为函数式接口的隐式成员,因为它们由函数式接口的实例自动实现。

你有引用来源的链接吗?谢谢! - uvsmtid
1
您可以在http://www.oracle.com/technetwork/java/newtojava/java8book-2172125.pdf中查看介绍Lambda的部分。 - Aniket Thakur

2

接口不能扩展Object类,因为接口必须有公共和抽象方法。

对于Object类中的每个公共方法,在接口中都存在一个隐式的公共和抽象方法。

这是标准的Java语言规范,如下所述:

“如果一个接口没有直接超级接口,则该接口会隐式声明一个公共抽象成员方法m,其签名为s,返回类型为r,并且与Object中声明的具有相同签名、相同返回类型和兼容抛出子句t的公共实例方法m相对应,除非该接口显式声明了具有相同签名、相同返回类型和兼容抛出子句的方法。”

这就是在接口中声明Object类方法的方式。根据JLS的规定,这并不算作接口的抽象方法。因此,Comparator接口是一个函数式接口。


1

一个函数式接口只有一个抽象方法,但可以有多个默认和静态方法。

由于默认方法不是抽象的,因此您可以自由地向您的函数式接口中添加任意数量的默认方法。

@FunctionalInterface
public interface MyFuctionalInterface 
{
public void perform();

default void perform1(){
//Method body
}

default void perform2(){
//Method body
}
}

如果一个接口声明了一个抽象方法,覆盖了java.lang.Object的公共方法之一,那么这也不算作该接口的抽象方法计数,因为接口的任何实现都将具有来自java.lang.Object或其他地方的实现。 Comparator是一个函数式接口,尽管它声明了两个抽象方法。因为其中一个抽象方法“equals()”具有与Object类中的公共方法相同的签名。 例如,下面的接口是有效的函数式接口。
@FunctionalInterface
    public interface MyFuctionalInterface 
    {
    public void perform();
 
    @Override
    public String toString();                //Overridden from Object class
 
    @Override
    public boolean equals(Object obj);        //Overridden from Object class
    }

0
这是一个“给我看代码”的方法来理解定义: 我们将研究OpenJDK javac如何检查使用@FunctionalInterface注释的类的有效性。
最新的(截至2022年7月)实现在此处: com/sun/tools/javac/code/Types.java#L735-L791
/**
 * Compute the function descriptor associated with a given functional interface
 */
public FunctionDescriptor findDescriptorInternal(TypeSymbol origin,
        CompoundScope membersCache) throws FunctionDescriptorLookupError {
    // ...
}

类限制

if (!origin.isInterface() || (origin.flags() & ANNOTATION) != 0 || origin.isSealed()) {
    //t must be an interface
    throw failure("not.a.functional.intf", origin);
}

相当简单:该类必须是一个接口,而且不能是密封的。
成员限制。
for (Symbol sym : membersCache.getSymbols(new DescriptorFilter(origin))) { /* ... */ }

在这个循环中,javac 遍历 origin 类的成员,使用 DescriptorFilter 检索以下内容:
  • 方法成员(当然)
  • && 是抽象的但不是默认的,
  • && 不覆盖 Object 的方法,
  • && 其顶层声明不是一个默认声明。

如果只有一个方法匹配所有上述条件,那么它肯定是有效的函数式接口,任何 lambda 都将覆盖该方法。

然而,如果有多个,javac 尝试合并它们:

  • 如果这些方法都有相同的名称,由{{link1:覆盖等价}}相关:
    • 然后我们将它们过滤到abstracts集合中:
      if (!abstracts.stream().filter(msym -> msym.owner.isSubClass(sym.enclClass(), Types.this))
              .map(msym -> memberType(origin.type, msym))
              .anyMatch(abstractMType -> isSubSignature(abstractMType, mtype))) {
          abstracts.append(sym);
      }
      
      如果方法符合以下条件,则会被过滤掉:
      • 它们的封闭类是另一个先前匹配的方法的超类,
      • 并且该先前匹配方法的签名是此方法的{{link2:子签名}}。
  • 否则,函数接口无效。

在收集了抽象后,javac然后进入mergeDescriptors,该方法使用mergeAbstracts,我将从其注释中引用:

/**
 * Merge multiple abstract methods. The preferred method is a method that is a subsignature
 * of all the other signatures and whose return type is more specific {@see MostSpecificReturnCheck}.
 * The resulting preferred method has a thrown clause that is the intersection of the merged
 * methods' clauses.
 */
public Optional<Symbol> mergeAbstracts(List<Symbol> ambiguousInOrder, Type site, boolean sigCheck) {
    // ...
}

结论

  1. 函数式接口必须是接口类型,并且不能被 sealed 或注解修饰。
  2. 方法在整个继承树中进行搜索。
  3. Object 中的方法重叠的方法将被忽略。
  4. default 方法将被忽略,除非它们被子接口作为非默认方法覆盖。
  5. 匹配的方法必须共享相同的名称,并且通过覆盖等价性相关联。
  6. 要么只有一个符合上述条件的方法,要么匹配的方法可以通过它们的类层次结构、子签名关系、返回类型和抛出的异常来“合并”。

-1
Java文档中说:

请注意,不覆盖Object.equals(Object)始终是安全的。但是,在某些情况下,覆盖此方法可能会通过允许程序确定两个不同的比较器施加相同的顺序来提高性能。

也许Comparator是特殊的?也许,即使它是一个接口,也有一种默认实现的equals()调用compare()吗?从算法上讲,这很简单。

我认为在接口中声明的所有方法都是抽象的(即没有默认实现)。但也许我漏掉了什么。


2
Comparator 并不是特殊的。当您创建一个新的 Comparator 对象时,它会继承自 Object,包括具体的 equals 方法。您关于接口中声明的所有方法都是抽象的评论在一般情况下已经不再正确,但在 Comparator 中的 equals 方法确实如此。我对 Mark Peters 回答的评论更详细地解释了这一点,或者请参阅 Lambda FAQ 页面上的语法注释 http://www.lambdafaq.org/what-is-a-functional-interface/。 - Maurice Naftalin

-1
在Java 8之前,接口只能声明一个或多个方法,也称为抽象方法(没有实现,只有签名)。从Java 8开始,接口可以具有一个或多个方法的实现(称为接口默认方法)和静态方法以及抽象方法。接口默认方法标记为default关键字。
那么问题是,什么是函数式接口? 具有单个抽象方法(SAM)的接口称为函数式接口。
这意味着 -
  1. 具有单个抽象方法的接口是函数式接口
  2. 具有单个抽象方法和零个或多个默认方法和零个或多个静态方法的接口也是有效的函数式接口。
更多详细信息和示例代码请参见https://readtorakesh.com/functional-interface-java8/

-1

定义:

如果一个接口只包含一个抽象方法,则这种类型的接口称为函数式接口。

用法:

  1. 一旦我们编写Lambda表达式“->”来调用其功能,那么在此上下文中,我们需要函数式接口。
  2. 我们可以使用函数式接口引用来引用Lambda表达式。
  3. 在函数式接口内部,我们可以有一个抽象方法和n个默认/静态方法。

关于继承的函数式接口:

如果一个接口扩展了函数式接口,并且子接口不包含任何抽象方法,则该子接口也被认为是函数式接口。

函数式接口对于Java并不新鲜,它已经在以下接口API中使用:

  1. Runnable:仅包含run()方法。
  2. Callable:仅包含call()方法。
  3. Comparable:仅包含compareTo()方法。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接