ChatGPT解决这个技术问题 Extra ChatGPT

何时使用:Java 8+ 接口默认方法,与抽象方法

Java 8 允许在名为 Default Methods 的接口中默认实现方法。

我对何时使用那种 interface default method 而不是 abstract class(带有 abstract method(s))感到困惑。

那么什么时候应该使用带有默认方法的接口,什么时候应该使用抽象类(带有抽象方法)?抽象类在那种情况下仍然有用吗?

也许你仍然不能在接口中拥有字段、私有方法等,而你可以在抽象类中?
我以前想知道这个话题,现在我清楚了。感谢@Narendra Pathai。我想添加您针对同一主题提出的另一个主题的链接,因为这两个都是我的疑问。 stackoverflow.com/questions/19998309/…
您可以在此找到一篇不错的帖子:blog.codefx.org/java/everything-about-default-methods
即使基类具有状态,有时您仍然可以将基类编码为接口。只是接口必须为状态定义setter和getter,具体的类必须实现它们并定义字段。对此的一个限制是,在抽象类中,bean 属性可以是私有的或受保护的。在接口中只有公共方法。因此,您使用抽象基类的一个原因是您的类是否具有需要私有或受保护的属性。
@DaBlick 你能不能通过 HashMap 解决接口中的状态问题。例如:如果你想要一个包含 int a、b、String c 的类 Foo。并且您希望它们具有状态,创建一个 HashMap< /*Foo 对象的名称*/ String, /*字段映射*/ Hashmap< /*name specific Field*/ String, /*field value*/ Object>> map .当您想“实例化”理论类 Foo 时,您有方法 instantiate(String nameOfFoo) 执行 map.put(nameOfFoo, fields) 其中 fields 是 HashMap fields.put("a", new诠释(“5”)); fields.put("b", new int("6")); fields.put("c", "blah"));

M
Marko Topolnik

抽象类比默认方法实现(例如私有状态)要多得多,但是从 Java 8 开始,只要您可以选择其中任何一种,就应该在接口中使用 defer(又名 default)方法。

默认方法的限制是它只能在调用其他接口方法的情况下实现,而不参考特定实现的状态。所以主要的用例是更高层次和方便的方法。

这个新特性的好处是,在你被迫使用抽象类来提供便利方法,从而将实现者限制为单一继承之前,现在你可以拥有一个非常干净的设计,只需要接口和最少的实现强加给程序员的努力。

default 方法引入 Java 8 的最初动机是希望在不破坏任何现有实现的情况下使用面向 lambda 的方法扩展 Collections Framework 接口。尽管这与公共图书馆的作者更相关,但您可能会发现相同的功能在您的项目中也很有用。您有一个集中的位置来添加新的便利,并且您不必依赖类型层次结构的其余部分的外观。


根据这个推理,他们接下来要添加的是默认方法声明。我仍然不确定这一点,该功能对我来说更像是一种黑客攻击,每个人都可以滥用它。
我能看到的 Java 8 时代抽象类的唯一用途是定义非最终字段。在接口中,字段默认为最终字段,因此一旦分配,您就无法更改它们。
@Anuroop 不只是默认情况下---这是唯一的选择。接口不能声明实例状态,这就是抽象类存在的原因。
@PhilipRego 抽象方法不调用任何东西,因为它们没有实现。类中实现的方法可以访问类的状态(实例变量)。接口无法声明它们,因此默认方法无法访问它们。他们必须依赖类提供访问状态的实现方法。
Marko Topolnik,你的答案已经死了。但我想建议对您的答案进行更新。您可能想补充一点,默认方法的美妙之处在于,如果接口添加了新的默认方法,您之前对该接口的实现不会中断。在 Java 8 之前,情况并非如此。
V
Vadym Vasyliev

存在一些技术差异。与 Java 8 接口相比,抽象类仍然可以做更多事情:

抽象类可以有一个构造函数。抽象类更加结构化并且可以保持状态。

从概念上讲,防御方法的主要目的是在 Java 8 中引入新特性(作为 lambda 函数)后向后兼容。


这个答案实际上是正确且有意义的,尤其是“从概念上讲,防御者方法的主要目的是向后兼容”
@UnKnown 此页面提供了更多见解:docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html
关于第 1 点更微妙的一点。以上2关于“能保持状态是这个”。抽象类可以保存以后可以更改的状态。接口也可以保持状态,但是一旦在实例创建后分配了状态,就不能更改状态。
@Anuroop 我不会将接口的 public static final 字段描述为“状态”。 static 部分表示与特定实例完全无关。它们被分配在类实例化时,这与在实例创建后不同。
我喜欢这个答案,但是“从概念上讲,防御方法的主要目的是在 Java 8 中引入新功能(作为 lambda 函数)后向后兼容”是什么意思?
M
Masudul

article中对此进行了描述。想想 forEach 的集合。

List<?> list = …
list.forEach(…);

java.util.List 和 java.util.Collection 接口还没有声明 forEach。一种明显的解决方案是将新方法添加到现有接口并在 JDK 中提供所需的实现。但是,一旦发布,就不可能在不破坏现有实现的情况下向接口添加方法。默认方法带来的好处是现在可以向接口添加新的默认方法,并且不会破坏实现。


'不可能在不破坏现有实现的情况下向接口添加方法' - 是吗?
@AndreyChaschev如果向接口添加新方法,则所有实现者都必须实现该新方法。因此,它打破了现有的实现。
@MarkoTopolnik 谢谢,错过了。顺便提一下,有一种方法可以部分避免这种情况 - 通过在默认抽象实现中呈现此方法。对于此示例,这将是 AbstractList::forEach 抛出一个 UnsupportedOperationException
@AndreyChaschev是的,那是旧方式(khm ...是当前方式:),其不足之处在于它限制实现者从提供的抽象实现进行单一继承。
如果提前发生这种情况,我不会中断,所有实现都包含该方法。这不太可能但可能。
S
Sufiyan Ghori

this 文章中所述,

Java 8 中的抽象类与接口

引入 Default Method 之后,似乎接口和抽象类是一样的。但是,它们在 Java 8 中仍然是不同的概念。抽象类可以定义构造函数。它们更加结构化,并且可以具有与之关联的状态。相比之下,默认方法只能在调用其他接口方法的情况下实现,而不参考特定实现的状态。因此,两者都用于不同的目的并在两者之间进行选择实际上取决于场景上下文。


我相信抽象类具有构造函数,它可以与接口不同。在 Java 8 中,它们也因此而彼此不同。
如果抽象类无法实例化,为什么它有构造函数?
我们可以从子类调用super(),它会调用抽象类的构造函数。这会影响抽象类的状态。
@GeorgeXavier 回答您关于“为什么抽象类有构造函数?”的问题,希望这可以对您的问题有所启发。谢谢......链接 1:(stackoverflow.com/questions/260666/…)......链接 2: (stackoverflow.com/questions/2170500/…)
a
akhil_mittal

每当我们在抽象类和接口之间进行选择时,我们应该总是(几乎)更喜欢默认(也称为防御者或虚拟扩展)方法。

默认方法结束了接口的经典模式和实现该接口中大部分或所有方法的伴随类。一个例子是 Collection 和 AbstractCollection。现在我们应该在接口本身中实现方法以提供默认功能。实现接口的类可以选择覆盖方法或继承默认实现。默认方法的另一个重要用途是接口演化。假设我有一个 Ball 类: public class Ball implements Collection { ... }

现在在 Java 8 中引入了一个新的特性流。我们可以使用添加到接口的 stream 方法来获取流。如果 stream 不是默认方法,则 Collection 接口的所有实现都会中断,因为它们不会实现这个新方法。向接口添加非默认方法不是 source-compatible

但是假设我们不重新编译该类并使用包含此类 Ball 的旧 jar 文件。如果没有这个缺失的方法,该类将正常加载,可以创建实例,并且似乎一切正常。 但是如果程序在 Ball 的实例上调用 stream 方法,我们将得到 AbstractMethodError。所以让方法默认解决了这两个问题。

Java 9 甚至在接口中有私有方法,可用于封装提供默认实现的接口方法中使用的公共代码逻辑。


A
Andrey Chaschev

这两个是完全不同的:

默认方法是将外部功能添加到现有类而不更改它们的状态。

抽象类是一种普通的继承类型,它们是旨在扩展的普通类。


U
Umar Tahir

尽管这是一个老问题,但我也可以就此发表意见。

抽象类:在抽象类内部我们可以声明实例变量,这是子类所必需的接口:在接口内部每个变量都是public static和final我们不能声明实例变量抽象类:抽象类可以谈论对象的状态接口:接口永远不能谈论对象抽象类的状态:在抽象类内部我们可以声明构造函数接口:在接口内部我们不能声明构造函数,因为构造函数的目的是初始化实例变量。那么如果我们在接口中不能有实例变量,那么那里的构造函数需要什么。抽象类:在抽象类中,我们可以声明实例和静态块接口:接口不能有实例和静态块。抽象类:抽象类不能引用 lambda 表达式接口:具有单个抽象方法的接口可以引用 lambda 表达式抽象类:在抽象类内部我们可以覆盖 OBJECT CLASS 方法接口:我们不能在接口内部覆盖 OBJECT CLASS 方法。

我将在结束时指出:

接口中的默认方法概念/静态方法概念只是为了保存实现类,而不是提供有意义的有用实现。默认方法/静态方法是一种虚拟实现,“如果你愿意,你可以使用它们,或者你可以在实现类中覆盖它们(在默认方法的情况下)”因此,每当接口中有新方法时,我们就不用在实现类中实现新方法被添加。因此接口永远不能等同于抽象类。


C
Community

关于您的查询

那么什么时候应该使用带有默认方法的接口,什么时候应该使用抽象类呢?抽象类在那种情况下仍然有用吗?

java documentation 提供了完美的答案。

抽象类与接口的比较:

抽象类类似于接口。您不能实例化它们,它们可能包含声明的带有或不带有实现的方法的混合。但是,使用抽象类,您可以声明非静态和最终的字段,并定义公共、受保护和私有的具体方法。使用接口,所有字段都自动是公共的、静态的和最终的,并且您声明或定义的所有方法(作为默认方法)都是公共的。此外,您只能扩展一个类,无论它是否是抽象的,而您可以实现任意数量的接口。

在下面的 SE 帖子中解释了它们中的每一个的用例:

What is the difference between an interface and abstract class?

抽象类在那种情况下仍然有用吗?

是的。它们仍然有用。它们可以包含非静态的、非最终的方法和属性(受保护的、私有的以及公共的),即使使用 Java-8 接口也是不可能的。


T
The Guy with The Hat

Java 接口中的默认方法支持接口演化。

给定现有接口,如果您希望在不破坏与旧版本接口的二进制兼容性的情况下向其添加方法,您有两个选择:添加默认方法或静态方法。实际上,添加到接口的任何抽象方法都必须由实现该接口的类或接口来实现。

静态方法是类独有的。默认方法对于类的实例是唯一的。

如果在现有接口中添加默认方法,则实现该接口的类和接口不需要实现它。他们能

实现默认方法,并覆盖已实现接口中的实现。

重新声明使其抽象的方法(没有实现)。

什么都不做(然后简单地继承实现接口的默认方法)。

有关主题 here 的更多信息。


N
Nicolas Zozol

Remi Forax 规则是 您不使用抽象类进行设计。您使用界面设计应用程序。 Waterever 是 Java 的版本,不管是什么语言。它由 SOLID 原则中的 I接口隔离原则 提供支持。

您可以稍后使用抽象类来分解代码。现在使用 Java 8,您可以直接在界面中执行此操作。这是一个设施,而不是更多。


A
Ahmad Sanie

什么时候应该使用带有默认方法的接口,什么时候应该使用抽象类?

向后兼容性:假设您的接口由数百个类实现,修改该接口将强制所有用户实现新添加的方法,即使它对于实现您接口的许多其他类可能不是必需的,而且它允许您的接口成为一个功能接口

事实与限制:

1-只能在接口中声明,而不是在类或抽象类中。

2-必须提供一个身体

3-它不像接口中使用的其他常规方法那样被假定为抽象的。


M
Manish Sahni

在 Java 8 中,接口看起来像一个抽象类,尽管它们可能存在一些差异,例如:

1) 抽象类是类,因此它们不受Java中接口的其他限制,例如抽象类可以有状态,但不能在Java中的接口上有状态。

2) 带有默认方法的接口和抽象类之间的另一个语义区别是,您可以在抽象类内部定义构造函数,但不能在 Java 中的接口内部定义构造函数


我同意#2,但对于#1,你不能只实现接口,从而通过实现类有一个状态吗?
a
anik

Java 接口中的默认方法更多地用于提供函数的虚拟实现,从而使该接口的任何实现类免于声明所有抽象方法的痛苦,即使它们只想处理一个抽象方法。因此,接口中的默认方法在某种程度上更多地替代了适配器类的概念。

然而,抽象类中的方法应该提供一个有意义的实现,任何子类只有在需要覆盖公共功能时才应该覆盖它。


j
johnnyodonnell

如其他答案中所述,添加了向接口添加实现的能力是为了在 Collections 框架中提供向后兼容性。我认为提供向后兼容性可能是向接口添加实现的唯一充分理由。

否则,如果您将实现添加到接口,您将违反最初添加接口的基本规律。 Java 是一种单继承语言,与允许多重继承的 C++ 不同。接口提供了支持多重继承的语言所带来的打字优势,而不会引入多重继承带来的问题。

更具体地说,Java 只允许实现的单一继承,但它确实允许接口的多重继承。例如,以下是有效的 Java 代码:

class MyObject extends String implements Runnable, Comparable { ... }

MyObject 仅继承一个实现,但它继承了三个协定。

Java传递了实现的多重继承,因为实现的多重继承带来了许多棘手的问题,这些问题超出了这个答案的范围。添加了接口以允许合约的多重继承(又名接口),而不会出现实现的多重继承问题。

为了支持我的观点,这里引用了来自 The Java Programming Language, 4th edition 一书的 Ken Arnold 和 James Gosling 的话:

单一继承排除了一些有用且正确的设计。多重继承的问题来自于实现的多重继承,但在很多情况下多重继承被用来继承许多抽象契约,也许是一个具体的实现。提供一种在不继承实现的情况下继承抽象契约的方法允许多重继承的打字优势,而不会出现多重实现继承的问题。抽象契约的继承称为接口继承。 Java 编程语言通过允许您声明接口类型来支持接口继承


A
Alferd Nobel

从业务用例上下文中,接口可用于定义特定的业务规则,而抽象类将定义启动业务的通用结构。

假设某个企业主想与 Amazon 和 Walmart 合作,那么此处定义的接口将是 WalmartPartnerAmazonPartner 将定义特定的业务规则,而抽象类 BusinessSetup 将获得特定区域的业务设置。

// Interfaces
 
public interface WalmartPartner {
    public static boolean signUpForWalmartBusinessAccount(String BusinessId){
        System.out.println("Setting up Walmart Business Partner");
        return true;
    }
    public default  void  getWalmartDeals(){
        System.out.println("Default walmart deal executed !");
    }
    public abstract void setupShopifyForWalmart();
    public abstract  void setupWalmartProducts();

public interface AmazonPartner {
    public static boolean signUpAsAmazonServicePartner(String BusinessId){
        System.out.println("Setting up Amazon Business Partner");
        return true;
    }
    public default  void  paymentPlatformSetup(){
        System.out.println(" Amazon default payment platform is setup");
    }
    public abstract void setupPrimeMemberDealsByRegion();
    public abstract  void setupPrimeDeals();
}

 // Abstract class 

public abstract class BusinessSetup {
    String businessId ;
    public BusinessSetup(String businessId){
        this.businessId = businessId;
        System.out.println("1. Initial Business setup for BusienssID: "+this.businessId+" is Complete");
    }
    public final boolean getBusinessRegisteredInRegion(String region){
        System.out.println("2. Business got registered in "+region+ "!");
        return true;
    }
    public abstract void setupCustomerPlatform(String customerId);
    public abstract void setupVendorPlatform(String vendorId);

}

// Concrete Class 
public class WalMartPartnerImpl extends BusinessSetup implements WalmartPartner {
    public WalMartPartnerImpl(String businessId) {
        super(businessId);
    }
    @Override
    public void setupCustomerPlatform(String customerId) {
    }

    @Override
    public void setupVendorPlatform(String vendorId) {
    }

    @Override
    public void setupShopifyForWalmart() {
    }

    @Override
    public void setupWalmartProducts() {
    }
    public static void main(String args[]){
        WalMartPartnerImpl walMartPartner = new WalMartPartnerImpl("wal8989");
        walMartPartner.getBusinessRegisteredInRegion("california");
        walMartPartner.getWalmartDeals();
        walMartPartner.setupCustomerPlatform("wal8989");

    }
}

P
Pang

请首先考虑开放/封闭原则。接口中的默认方法确实违反了它。这是 Java 中的一个坏特性。它鼓励糟糕的设计、糟糕的架构和低软件质量。我建议完全避免使用默认方法。

问自己几个问题:为什么不能将方法放到抽象类中?那么你需要一个以上的抽象类吗?然后想想你的班级负责什么。您确定要放在单个类中的所有方法都真正实现相同的目的吗?可能你会区分几个目的,然后将你的班级分成几个班级,每个目的都有自己的班级。