Lecture from: 29.11.2024 | Video: Videos ETHZ
Interfaces
Interfaces in Java define a contract that classes can implement. They specify a set of methods that implementing classes must provide. Interfaces are a powerful mechanism for achieving abstraction and polymorphism in object-oriented programming. They are sometimes referred to as “interfaces” or “Schnittstellen”.
Motivation: Collections and Multiple Inheritance
One of the primary motivations for interfaces is to overcome the limitations of single inheritance in Java. Consider the example of Java’s Collections Framework:
Graphics from: https://en.wikipedia.org/wiki/Java_collections_framework
The Collections Framework uses interfaces like Collection
, List
, Set
, and Queue
to define common operations for different collection types. This allows you to write generic code that can work with any collection that implements the appropriate interface.
Imagine a scenario where you want to represent a “student teaching assistant”. You might consider defining a new class that inherits from both Student
and TA
:
However, Java does not support multiple inheritance of classes. This is where interfaces come in. Interfaces offer a way to achieve a form of multiple inheritance, allowing a class to implement multiple interfaces.
Interfaces: The Basics
An interface in Java is a blueprint or contract that classes can adhere to. It declares a set of methods but provides no implementation for them. Interfaces can also contain constants (public static final fields).
Interface Declaration:
- No Implementation: Interface methods are declared without a body (just a semicolon). They are implicitly
public
andabstract
. Note: Starting with Java 8, interfaces can also have default methods and static methods with implementations, but we’ll ignore these for now. - Constants: Interface fields are implicitly
public
,static
, andfinal
—effectively constants. - Visibility: The interface itself can be public or package-private. Interface methods are always public and abstract.
Implementing Interfaces
A class can implement one or more interfaces using the implements
keyword. The class must provide concrete implementations for all methods declared in the interfaces it implements.
Example Vehicle
Example List
Example Shape
Interfaces and Subtyping
An interface defines a new type. Even though you cannot create instances of an interface directly (they are abstract), you can declare reference variables of interface types. These variables can refer to objects of any class that implements the interface. This exemplifies subtyping: A class that implements an interface “is-a” type of that interface.
Subtyping and Dynamic Binding: Similar to inheritance, dynamic binding applies when calling methods on interface references. The actual version of the method executed at runtime is determined by the dynamic type (the object’s class) of the reference.
Interfaces: A Guarantee
Interfaces provide a guarantee: if a variable has an interface type, you can be certain that the object it refers to implements all the methods declared in that interface. This makes interfaces a powerful tool for enforcing contracts between different parts of your code.
Abstract Classes vs. Interfaces: A Comparison
Both abstract classes and interfaces are used to achieve abstraction in Java, but they have key differences:
Feature | Abstract Class | Interface |
---|---|---|
Implementation | Can have both abstract and concrete methods. | All methods are implicitly abstract (before Java 8). |
Attributes | Can have instance variables. | Can only have constants (public static final). |
Inheritance | A class can extend only one abstract class. | A class can implement multiple interfaces. |
Relationship Type | ”is-a” relationship (inheritance) | “can-do” relationship (implementation) |
Main Purpose | Provide a common base with partial implementation. | Define a contract or specification. |
Choosing between Abstract Classes and Interfaces:
- Use an abstract class when you want to provide a common base with some shared implementation for a group of subclasses.
- Use an interface when you want to define a contract that multiple unrelated classes can implement.
Interfaces and Inheritance
Interfaces can participate in inheritance hierarchies just like classes, but using the extends
keyword. An interface can extend one or more other interfaces, inheriting all their methods and constants.
Interface Inheritance:
-
Subtyping: The subinterface (e.g.,
SubInterface
) becomes a subtype of all its superinterfaces (e.g.,SuperInterface1
,SuperInterface2
). -
Implementation Requirement: A class implementing the subinterface must provide concrete implementations for all methods declared in both the subinterface and its superinterfaces.
This example shows a simple case of one interface inheriting from another and a slightly more complex case where an interface extends multiple interfaces. This way interfaces allow us a form of multiple inheritance!
Interfaces, Inheritance and classes
A class that extends another class which implements some interface does not need to declare this implementation explicitly. It gets inherited and the class automatically has to implement all methods specified by the interface unless its abstract.
Implementing Multiple Interfaces
A class can implement multiple interfaces, separated by commas. This is a way to achieve a form of multiple inheritance in Java, where a class can inherit behavior (method signatures) from multiple sources.
WaterCar
implements both Car
and Boat
, inheriting methods from each.
Extending Interfaces
Similar to classes, interfaces can extend other interfaces using the keyword extends
. The extending interface inherits all methods and constants from the superinterface, just like multiple inheritance. You can then implement this new interface in a class. This is a form of creating new interfaces based on existing ones. This is an important concept, as you will see in a later example.
In this more complex example, Amphibian
inherits from both Car
and Boat
, which both inherit from Vehicle
.
Inheritance, Abstract Classes, and Interfaces: A Summary
These three concepts are fundamental to object-oriented programming in Java and play distinct roles in achieving code reuse and polymorphism.
-
Inheritance: Provides both implementation inheritance (reusing code from a superclass) and subtyping (treating objects of different classes as instances of a common type).
-
Abstract Classes: Offer partial implementation and subtyping. They can have both abstract methods (no body) and concrete methods (with a body). Used to define a common base class with some shared implementation, leaving certain aspects for subclasses to complete.
-
Interfaces: Focus exclusively on subtyping. They define a contract or specification by declaring method signatures without providing implementations. Promote loose coupling and polymorphism by allowing objects of different classes to be used interchangeably as long as they implement the same interface.
Key Differences
Feature | Inheritance | Abstract Classes | Interfaces |
---|---|---|---|
Implementation | Full | Partial | None |
Subtyping | Yes | Yes | Yes |
Multiple Inheritance (of type/implementation) | No | No | Yes |
Essentially, inheritance enables code reuse and subtyping, abstract classes refine inheritance with the flexibility of abstract members, and interfaces concentrate purely on defining type contracts for maximum flexibility and polymorphism.
The Java Collections Framework
The Java Collections Framework provides a set of interfaces and classes that implement commonly used data structures. It offers a standardized way to store, manipulate, and retrieve collections of objects, making it easier to write efficient and reusable code.
Data Structures and the Java Collections Framework
A data structure is a specialized format for organizing, processing, retrieving and storing data. It allows efficient access and modification of data. The structure determines the kinds of operations that can be performed efficiently, as well as the efficiency of those operations.
Typical Operations
-
Queries: These are operations that retrieve information from the data structure without modifying it. Examples include
contains()
(checking if an element is present) andsize()
(getting the number of elements). -
Modifications: These operations change the data structure. Examples include
add()
(inserting an element),remove()
(deleting an element), andclear()
(removing all elements).
Properties of Data Structures
Data structures can have several important properties:
- Ordered: Elements have a specific sequence or order. Examples include lists and queues.
- Indexed: Elements can be accessed directly by their index (position), also known as Random Access. Arrays and
ArrayList
are examples. - Sorted: Elements are arranged according to some ordering (e.g., numerical or alphabetical).
TreeSet
andTreeMap
maintain sorted order. - Duplicate-free: Each element appears only once.
Set
implementations ensure uniqueness. - Associative: Elements are stored as key-value pairs.
Map
implementations are associative.
The java.util.Collections
Class
The java.util.Collections
class provides static utility methods for working with collections. These methods offer functionality similar to the methods available for arrays (e.g., sorting, copying). These utility methods make operations across different collection types easier and promote code consistency.
Collection Types: Collection
and Map
The Java Collections Framework has two main interface hierarchies:
-
java.util.Collection
: For non-associative data structures. This includes interfaces likeList
,Set
, andQueue
. These data structures only store values. -
java.util.Map
: For associative data structures, which store key-value pairs. These data structures act like dictionaries or tables. The keys should form aSet
(no duplicates) while any number of keys can map to the same value.
Interface Collection
The Collection
interface is the root of the non-associative collection hierarchy. It defines core methods like add()
, remove()
, contains()
, size()
, isEmpty()
, and clear()
, which are common to all collection types.
The <E>
indicates that Collection
is a generic interface (we’ll cover generics later).
Interface List
The List
interface represents an ordered collection of non-associative elements, allowing duplicates. It extends the Collection
interface and adds methods for working with indexed elements, such as get()
, set()
, add(int index, E element)
, and remove(int index)
.
More here: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/List.html
Note that not all methods provided by the List interface are intended for frequent use. The documentation recommends using specific operations based on the characteristics of the implementing classes. The existence of a method doesn’t imply efficiency!
LinkedList
LinkedList
implements the List
interface using a doubly linked list data structure. This means each element stores references to both the previous and next elements in the list.
-
Efficient Insertion/Deletion at the Beginning/End: Adding or removing elements at the beginning or end of a
LinkedList
is very efficient ( time complexity). -
Inefficient Index-Based Access: Accessing elements by their index requires traversing the list from the beginning or end, which can be slow ( time complexity where n is the number of elements).
-
Suitable for Stack and Queue Implementations:
LinkedList
is well-suited for implementing stack and queue data structures due to its efficient insertion/deletion characteristics at both ends.
ArrayList
ArrayList
implements the List
interface using a dynamic array. This means it stores elements in a contiguous block of memory, and the array automatically resizes as needed.
- Efficient Random Access: Accessing elements by index is very fast ().
- Amortized Insertion/Deletion at the End: Adding or removing elements at the end of an
ArrayList
is usually efficient, but occasionally requires resizing the underlying array, which is a more expensive operation. Amortized means that even with these resize operations, the average time complexity is close to . - Inefficient Insertion/Deletion at the Beginning: Inserting or deleting at the beginning requires shifting all subsequent elements, which can be slow ().
Creating New Lists
When creating new ArrayList
or LinkedList
objects, you specify the type of elements the list will hold using type parameters (within angle brackets <>
). Since Java 7 the Diamond operator may be used to infer the type arguments by the compiler. It is also possible to create a list from an existing collection or from a fixed-size list generated using Arrays.asList()
.
Lists and Types
ArrayList<E>
, LinkedList<E>
, and List<E>
all define new types. You can use these types as:
Attributes (instance variables)
Local variables
Method parameters
Method return types
Collections and Subtyping
Review:
-
A reference variable of a class type
K
can hold references to instances ofK
or any of its subclasses. This is regular upcasting. The reverse requires an explicit downcast. -
A reference variable of an interface type
X
can only hold references to instances of classes that implement that interface.
This applies equally to generic classes and interfaces:
Example:
Subtyping and Type Parameters: A Caveat
Even if class B
is a subtype of class A
, a List<B>
is not a subtype of List<A>
.
Example:
However, a Collection
or List
with the most general Object
type parameter can store instances of both subtypes A
and B
:
Collection Types: Recommendation
For maximum flexibility, it’s generally recommended to declare variables and parameters using the most general interface or abstract type (e.g., List<Integer>
or Collection<E>
) rather than concrete implementation types (e.g., ArrayList<Integer>
or LinkedList<E>
). This allows you to switch between different implementations later without changing much of your code. This can postpone choices about concrete implementations, promoting flexibility in program design.
Caveat: The choice of concrete collection implementation can significantly affect runtime performance. Be mindful of the performance characteristics of different collection types when making your choices.
f(list1)
will likely run faster than f(list2)
due to the different access patterns in ArrayList
(random access is efficient) vs LinkedList
(sequential access).
Iterating and Modifying Collections: Caution!
Be extremely careful when both iterating over a list and modifying it at the same time. Modifying during iteration can lead to undesired behavior. Improper modifications during iterations can cause exceptions or infinite loops.
The first example results in an infinite loop because numbers.size()
grows with each iteration. The second throws IndexOutOfBoundsException
as we remove elements and hence reduce the list’s size. For example we delete element 0 in the list [0, 12]
. Then when we want to access element 1 in [12]
(size is 2!), we get an error since the index 1 is out of bounds for list of length 1.
Example Intersection
Variant 1 (Creating a new list)
We’ll look at more further variations next time…
Continue here: 21 Interface List, Comparing Elements, Interface Set, Hashing and Hashcode