Interface is a contract defining a set of abstract methods that a class must implement. In Java and Kotlin, interfaces are the primary mechanism for abstraction and polymorphism. In Java 8+, interfaces can contain default and static methods, in Kotlin — default implementations. According to Google Android Developers (2025), interfaces are used in 90% of Android projects to define architecture layers — repositories, UseCases, and services.
Key Takeaways
Interface is a reference type that contains abstract methods, constants, and default methods. A class implementing an interface must provide implementations for all its abstract methods. In Java, an interface cannot have state (instance fields); Kotlin also adheres to this restriction but supports properties with accessors.
public interface Repository {
T findById(Long id);
List findAll();
T save(T entity);
void deleteById(Long id);
default long count() {
return findAll().size();
}
}
@Entity
public class UserEntity {
private Long id;
private String email;
// getters and setters
}
public class UserRepositoryImpl implements Repository {
private final EntityManager em;
public UserEntity findById(Long id) {
return em.find(UserEntity.class, id);
}
public List findAll() {
return em.createQuery("FROM UserEntity", UserEntity.class)
.getResultList();
}
// other methods
}Repository<T> is a generic interface with CRUD methods. The default method count() provides a default implementation that can be overridden. UserRepositoryImpl implements the interface using EntityManager for data access. This approach allows testing the data layer through an interface mock without a real database.
The choice between an interface and an abstract class depends on the presence of shared state and the relationships between types. An interface defines a contract (what a class can do), an abstract class defines a common implementation (what a class is).
| Criteria | Interface | Abstract Class |
|---|---|---|
| State (fields) | Only static final constants | Yes, any fields |
| Constructors | No | Yes |
| Multiple inheritance | Yes (implements) | No (extends one) |
| Access modifiers | public (Java 8-), default methods | All (private, protected, public) |
| When to use | Contract for unrelated classes | Common base for related classes |
In Clean Architecture, interfaces are placed in the inner layer (domain), and implementations in the outer layer (data). This allows maintaining the Dependency Rule: the outer layer depends on the inner layer, but not vice versa. Abstract classes are more often used for template methods (Template Method pattern).
Kotlin interfaces are more flexible than Java: they can declare abstract properties and provide method implementations. Unlike Java, Kotlin supports delegation via the by keyword, which powerfully reduces boilerplate when implementing the Delegate pattern.
interface ApiService {
val baseUrl: String // abstract property
suspend fun fetchData(): Result>
fun getEndpoint(path: String): String {
return "$baseUrl/$path" // default implementation
}
}
class RetrofitApiService(
override val baseUrl: String
) : ApiService {
private val client = Retrofit.Builder()
.baseUrl(baseUrl)
.build()
override suspend fun fetchData(): Result>
{
// request implementation
}
}
// Delegation via by
interface Logger {
fun log(message: String)
}
class ConsoleLogger : Logger {
override fun log(message: String) = println(message)
}
class UserService(logger: Logger) : Logger by loggerUserService implements the Logger interface through delegation (by). All log() calls are forwarded to the logger object without writing wrapper methods. This is an example of composition that in Java would require 5-10 lines of boilerplate code.
Clean Architecture for Android clearly separates the application into layers. Interfaces act as boundaries between layers: Domain defines repository and UseCase interfaces, Data provides implementations. This allows replacing implementations without changing business logic — a key advantage when migrating from Room to Firebase or from REST to GraphQL.
// Domain layer — interface (contract)
interface UserRepository {
suspend fun getUser(id: String): User
suspend fun updateUser(user: User)
}
// Domain layer — use case (depends on interface)
class GetUserUseCase(
private val repository: UserRepository
) {
suspend operator fun invoke(id: String): Result {
return runCatching { repository.getUser(id) }
}
}
// Data layer — implementation
class UserRepositoryImpl(
private val api: UserApi,
private val dao: UserDao
) : UserRepository {
override suspend fun getUser(id: String): User {
val cached = dao.getUser(id)
if (cached != null) return cached
val remote = api.fetchUser(id)
dao.insertUser(remote)
return remote
}
// updateUser...
}UserRepository is an interface in the domain layer. GetUserUseCase depends on the interface, not the implementation. UserRepositoryImpl in the data layer implements the interface, combining API and local database. This architecture allows testing UseCases with a mock repository without setting up a database or network.
Default methods were added in Java 8 for evolutionary extension of interfaces without breaking backward compatibility. If ArrayList did not implement the new stream() method added to Collection, old code would continue to work. Static methods in interfaces serve as utilities related to the interface — an alternative to utility classes.
public interface Vehicle {
void start();
void stop();
default void honk() {
System.out.println("Beep!");
}
static Vehicle of(String type) {
if ("car".equals(type)) return new Car();
return new Bicycle();
}
// constant
String CATEGORY = "transport";
}Default methods solve the diamond problem: if a class implements two interfaces with the same default method, the compiler requires explicit overriding. Static methods are called through the interface name — Vehicle.of("car"), without an instance.
Mistakes in interface design lead to fragile code, testing complexity, and SOLID violations. Let's look at three common problems.
An interface containing 15+ methods violates the Interface Segregation Principle (ISP). An example is the old java.util.Dictionary with 10+ methods. Solution: split into several small interfaces — ReadableRepository, WritableRepository, SearchableRepository. A client (service) depends only on the methods it needs.
Creating an interface for every class without a real need for polymorphism is the Interface overkill antipattern. Indicator: the interface has exactly one implementation, and the project has no plans to add alternatives. Solution: add an interface only when a second implementation option appears or a mock is needed for testing.
Frequently Asked Questions
Interface defines only a contract (method signatures), cannot have state, and supports multiple inheritance. Abstract class can contain fields, constructors, and implemented methods, but a class can inherit only one abstract class. Since Java 8, interfaces have gained default and static methods, narrowing the gap.
Yes, in Java and Kotlin interfaces support inheritance. public interface AdvancedRepository<T> extends Repository<T>, Pageable is an interface combining two others. A class implementing AdvancedRepository must implement all methods of both parent interfaces. Multiple inheritance is allowed only for interfaces.
Functional interface is an interface with a single abstract method (SAM — Single Abstract Method). The @FunctionalInterface annotation guarantees this restriction. Examples: Runnable, Callable, Comparator, Consumer. Functional interfaces are the foundation of Java 8 lambda expressions: () -> System.out.println() implements Runnable.
Default methods allow adding new methods to an interface without modifying all implementing classes. For example, Java 8 added stream() to Collection as a default method. Without this mechanism, foreach(), stream(), and other methods would require changes to thousands of classes in the JDK. Default is a backward-compatible way of extension.
Kotlin prohibits multiple class inheritance but allows multiple interface implementation. If two interfaces have a method with the same signature and default implementation, the compiler requires explicit overriding with super<InterfaceName>.method() call. This resolves the diamond problem at the compilation level.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also