13 Discuss the Differences Between Interfaces and Abstract Classes

接口和抽象类是面向对象设计中的两个重要概念,在Java中也经常被使用到。它们的区别主要体现在以下几个方面:

  • 定义方式不同: 接口是用interface关键字定义的,而抽象类是用abstract class关键字定义的。

  • 实现方式不同: 在Java中,一个类可以实现多个接口,但只能继承一个抽象类。

  • 方法实现的要求不同: 在接口中,定义的所有方法都是抽象的,没有方法体;而在抽象类中,可以定义抽象方法(没有方法体)和非抽象方法(有方法体)。

  • 字段的支持不同: 接口中只能定义常量字段,而抽象类中可以定义常量字段和非常量字段。

  • 构造方法的支持不同: 接口中不能有构造方法,而抽象类中可以有构造方法。

  • 设计目的不同: 接口的设计目的是为了定义一组行为规范,使得不同的类可以遵循这些规范来进行开发;抽象类的设计目的是为了提供一种通用的基类,封装通用的属性和行为,供子类继承和实现。

要注意的是,在某些情况下,接口和抽象类可以相互替代使用。当多个类具有相似的行为规范,并且这些类之间不存在继承关系时,可以使用接口来定义这些规范。而当多个类之间存在继承关系,并且需要共享一些通用的属性和方法时,可以使用抽象类来实现。

希望以上内容能够帮助你理解接口和抽象类的区别。

Typical Answer #

Interfaces and abstract classes are two fundamental mechanisms in Java object-oriented design.

An interface is an abstraction of behavior. It is a collection of abstract methods. Interfaces are used to separate the definition of an API from its implementation. Interfaces cannot be instantiated and cannot contain any non-constant members. All fields in an interface are implicitly public, static, and final. Interfaces do not have any non-static method implementations, which means that they are either abstract methods or static methods. The Java standard class library defines many interfaces, such as java.util.List.

An abstract class is a class that cannot be instantiated. It is marked with the abstract keyword. The main purpose of an abstract class is code reuse. Apart from the inability to be instantiated, an abstract class is not much different from a regular Java class. It can have one or more abstract methods, or it may not have any abstract methods at all. Abstract classes are mainly used to extract common method implementations or common member variables from related Java classes, and then achieve code reuse through inheritance. In the Java standard library, for example, the collection framework has many common parts that are extracted into abstract classes, such as java.util.AbstractList.

In Java, a class implements an interface using the implements keyword, and it extends an abstract class using the extends keyword. We can refer to the ArrayList class in the Java standard library as an example.

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    //...
}

Exam Analysis #

This is a very high-frequency Java object-oriented basic question. It appears to be a very simple question, but if the interviewer goes a little deeper, you will find many interesting aspects that can comprehensively assess your understanding and mastery of basic mechanisms. For example:

  • Whether you have a precise understanding of the syntax of Java’s basic elements. Can you define interfaces, abstract classes, or related inheritance implementations with basic correct syntax? It involves various different topics such as overloading and overriding.
  • Proper use of interfaces and abstract classes in software design and development. You should at least know typical application scenarios and master the use of important interfaces in the basic class library. You should also understand design methods and be able to identify obvious designs that are unfavorable for future maintenance when reviewing code.
  • Mastery of the Java language’s feature evolution. Many frameworks nowadays are already based on Java 8 and gradually support newer versions. It is necessary to master the relevant syntax and understand the design purposes.

Knowledge Expansion #

I will elaborate on some expansion points from the practice of interfaces, abstract classes, and language changes.

In comparison to other object-oriented languages like C++, Java has some fundamental differences in its design, such as Java not supporting multiple inheritance. This limitation not only regulates code implementation but also introduces some constraints that affect program design structure. Java classes can implement multiple interfaces because interfaces are collections of abstract methods, so this is declarative. However, logic cannot be reused by extending multiple abstract classes.

In certain scenarios, there may be a need to abstract out generic logic that is independent of concrete implementation or instantiation, or to handle pure calling relationships. Traditional abstract classes would face problems in a single inheritance environment. In the past, a common solution was to implement utility classes (Utils) composed of static methods, like java.util.Collections.

Consider that if we add any abstract methods to an interface, all classes implementing this interface must also implement the new methods, otherwise, compilation errors will occur. For abstract classes, if we add non-abstract methods, their subclasses will only benefit from the enhanced capability without worrying about compilation errors.

The responsibility of an interface is not limited to being a collection of abstract methods, and it actually has various practices. There is a specific type of interface with no methods, commonly known as a Marker Interface. As the name suggests, its purpose is to declare something, such as the well-known Cloneable, Serializable, and others. This usage can also be found in other Java product code in the industry.

At first glance, this may seem similar to annotations, and indeed it is. The advantage of Marker Interfaces is that they are simple and direct. Annotations, on the other hand, are more powerful in terms of expressing capability because they can specify parameters and values, which is why more people choose to use annotations.

Java 8 introduced support for functional programming, which added a new type of interface called a functional interface, which simply means an interface with only one abstract method. It is usually recommended to use the @FunctionalInterface annotation to mark it. Lambda expressions themselves can be seen as a type of functional interface, and to some extent, this is different from object-oriented programming. Well-known examples of functional interfaces include Runnable and Callable, and I won’t go into too much detail here. If you are interested, you can refer to: https://www.oreilly.com/learning/java-8-functional-interfaces

There is another surprising fact that strictly speaking, interfaces can also have method implementations in Java 8 and later!

Starting from Java 8, interfaces added support for default methods. Starting from Java 9, private default methods can also be defined. Default methods provide a way to extend existing interfaces binary-compatible. For example, the well-known java.util.Collection, which is the root interface of the collection framework, added a series of default methods in Java 8, mainly to add Lambda and Stream-related features. Many methods in utility classes like Collections mentioned earlier are suitable to be implemented as default methods in the basic interface.

You can refer to the following code snippet:

public interface Collection<E> extends Iterable<E> {
    /**
    * Returns a sequential Stream with this collection as its source 
    * ...
    **/
    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }
}

Object-Oriented Design

When it comes to object-oriented programming, many people think of design patterns, which are summaries of classic problems and design methods. Today, I want to solidify the basics and talk about the fundamental aspects of object-oriented design. We must be clear about the basic elements of object-oriented programming: encapsulation, inheritance, and polymorphism.

The purpose of encapsulation is to hide the implementation details of an object so as to improve security and simplify programming. Encapsulation provides a reasonable boundary, preventing external callers from accessing the internal details. In our daily development, there are often difficult-to-resolve bugs that are caused by unintentionally exposing details, such as exposing internal states in a multi-threaded environment, resulting in concurrency issues. From another perspective, this kind of hiding provided by encapsulation also simplifies the interface, avoiding unnecessary details that waste the caller’s effort.

Inheritance is the fundamental mechanism for code reuse, similar to the generalization of horses, white horses, and black horses. However, it is important to note that inheritance can be seen as a tightly coupled relationship, where changes in the parent class code will also affect the behavior of the child class. In practice, excessive use of inheritance can have a counterproductive effect.

Polymorphism, you may immediately think of overriding, overloading, and upcasting. Simply put, overriding refers to methods in the parent and child classes that have the same name and parameters but different implementations, while overloading refers to methods with the same name but different parameters. Fundamentally, these method signatures are different. To better illustrate, please refer to the example code below:

public int doSomething() {
    return 0;
}
// Different input parameters, which means different method signatures, demonstrating overloading
public int doSomething(List<String> strs) {
    return 0;
}
// Different return type, compilation error
public short doSomething() {
    return 0;
}

Here, you can think about a question: if the method name and parameters are the same, but the return values are different, is this considered valid overloading in Java code? The answer is no, it will result in compilation errors.

When doing object-oriented programming, it is necessary to master the basic design principles. Today, I will introduce the most common ones, known as the S.O.L.I.D principles.

  • Single Responsibility: A class or object should ideally have only one responsibility. If you find that a class has multiple responsibilities, consider splitting it.
  • Open-Closed Principle: Design should be open for extension but closed for modification. In other words, program design should ensure smooth extensibility and minimize the need to modify existing implementations when adding similar features, thus reducing regression issues.
  • Liskov Substitution Principle: This is one of the basic elements of object-oriented programming. When abstracting inheritance relationships, anywhere a parent class or base class can be used, a child class can be used as well.
  • Interface Segregation Principle: When designing classes and interfaces, if a single interface has too many methods, its subclasses may face a dilemma, where only a subset of the methods is meaningful to them, thereby compromising the cohesiveness of the program.

In such cases, multiple interfaces with a single responsibility can be created to decouple the behaviors. In future maintenance, if any changes are made to a particular interface, it will not affect the subclasses that use other interfaces.

  • Dependency Inversion: Entities should depend on abstractions rather than implementations. This means that high-level modules should not depend on low-level modules, but should instead rely on abstractions. Practicing this principle is the key to ensuring appropriate coupling between code components.

Trade-offs in implementing OOP principles

It is worth noting that in the development of modern languages, the principles mentioned earlier are not always strictly followed. For example, Java 10 introduced local variable type inference and the var keyword. According to the Liskov Substitution Principle, we usually define variables like this:

List<String> list = new ArrayList<>();

By using the var type, it can be simplified to:

var list = new ArrayList<String>();

However, the list is actually inferred as “ArrayList ”:

ArrayList<String> list = new ArrayList<String>();

In theory, this syntax convenience actually enhances the program’s dependency on implementation. But the slight type leakage brings the convenience of writing and improves code readability. Therefore, in practice, we need to make choices based on the pros and cons, rather than blindly following the principles.

Analysis of OOP principles in interview questions

In my past interviews, I have found that even engineers with years of programming experience have not truly mastered the basic principles of object-oriented design, such as the Open-Closed Principle. Take a look at the following code, adapted from a piece of code circulating in a famous company’s product. How can we improve it using the principles of object-oriented design?

public class VIPCenter {
  void serviceVIP(T extend User user>) {
     if (user instanceof SlumDogVIP) {
        // Poor X VIP, those who grab activities
        // do something
      } else if(user instanceof RealVIP) {
        // do something
      }
      // ...
  }

One problem with this code is that the business logic is concentrated in one place. When a new user type appears, for example, if big data discovers that we are lucrative targets and needs to harvest from us, this would require directly modifying the service method’s code implementation, which could inadvertently affect the logic of an unrelated user type.

By using the Open-Closed Principle, we can try to refactor the code as follows:

public class VIPCenter {
   private Map<User.TYPE, ServiceProvider> providers;
   void serviceVIP(T extend User user) {
      providers.get(user.getType()).service(user);
   }
 }
 interface ServiceProvider{
   void service(T extend User user) ;
 }
 class SlumDogVIPServiceProvider implements ServiceProvider{
   void service(T extend User user){
     // do something
   }
 }
 class RealVIPServiceProvider implements ServiceProvider{
   void service(T extend User user) {
     // do something
   }
 } 

In the above example, we abstract the service methods for different object categories, breaking the tight coupling of business logic and ensuring easy extension by isolating the implementation code.

Today, I have organized Java object-oriented technology, compared abstract classes and interfaces, analyzed the evolution of interfaces in Java and their corresponding program design implementations. Finally, I have reviewed and put into practice the basic principles of object-oriented design. I hope this helps you.

Practice Exercise #

Have you grasped the difference between interfaces and abstract classes? I will give you a question to ponder. Reflect on your own product code and see if there are any places where you violate basic design principles. Do those code snippets that collapse upon slight modification adhere to the open-closed principle?

Please write your thoughts on this question in the comments section. I will choose carefully considered comments and reward them with a learning encouragement prize. Feel free to discuss with me.

Are your friends also preparing for interviews? You can “ask friends to read” and share today’s topic with them. Perhaps you can help them.