×
Community Blog Fall Into the Trap of Java Interface

Fall Into the Trap of Java Interface

This article introduces the experience of encountering errors in code and the potential for one line of code to have different effects in different scenarios.

By Zhoupan

1

First of all, please take a moment to read the following code blocks to see if there are any problems or risks in the code.

PostTask.java

public interface PostTask {
    void process();
}

BaseResult.java

public interface BaseResult extends Serializable {
    List<PostTask> postTaskList = Lists.newArrayList();
    default void addPostTask(PostTask postTask) {
        postTaskList.add(postTask);
    }
    default List<PostTask> getPostTaskList() {
        return postTaskList;
    }
}

SimpleResult.java

public class SimpleResult implements BaseResult {
}
// The logic of request processing
SimpleResult result = new SimpleResult();
...
// During processing, tasks are added to the post-task list.
result.addPostTask(() -> { ...send a message... });
...
// Before the results are returned, all post-tasks are traversed and executed.
PostTaskUtil.process(result.PostTaskList());
...

PostTaskUtil.java

public class PostTaskUtil {
    public static void process(List<PostTask> postTasks) {
        if(CollectionUtils.isEmpty(postTasks)){
            return;
        }
        Iterator<PostTask> iterator = postTasks.iterator();
        while (iterator.hasNext()){
            PostTask postTask = iterator.next();
            if (postTask == null) {
                return;
            }
            postTask.process();
            iterator.remove();
        }
    }
}

If you find all the problems and risks, then congratulations, you have a solid understanding of the knowledge. 👆🏻 Let's step by step to explore code problems and risks and review the basic knowledge 😄.

The Attributes of the Interface are public static final Modified

Let's review the knowledge points of interface attributes. The following content is from the Oracle Java tutorial.

In addition, an interface can contain constant declarations. All constant values defined in an interface are implicitly public, static, and final. Once again, you can omit these modifiers.

The problem with the above code lies in the attribute postTaskList of the interface BaseResult. Since it is an attribute of the interface, it is static by default, which means that the post-task list operated by all instantiated SimpleResults has the same underlying queue. This is the biggest issue.

Since the number of requests for the problematic code is very small and the post-task is to send messages, although the above code problem results in the sending of many duplicate messages, the downstream systems process these messages in an idempotent manner. Therefore, nothing happened for the time being and this problem is quickly solved later.

2. Issue Analysis

While the author's experience is fortunate, the above issue becomes quite severe in actual scenarios with high concurrency. Let's analyze them one by one.

• The elements of the post-task list are uncertain and may contain historical or other requested tasks.

• Concurrent modification exception: java.util.ConcurrentModificationException.

The author provides a piece of test code, and you can run it to see how it works (the author runs it using JDK 22). The code uses the latest features: virtual threads and string templates. You can also learn new knowledge points and learn new things from the past 😄.

Test Code

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;

public class InterfaceBugTest {
    public static void main(String[] args) throws InterruptedException {
        List<CompletableFuture<Boolean>> futures = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            CompletableFuture<Boolean> future = CompletableFuture.supplyAsync(() -> {
                Test test = new Test();
                test.add(finalI);
                System.out.println(STR."\{finalI}: \{test.list.toString()}");
                return true;
            }, Executors.newVirtualThreadPerTaskExecutor());
            futures.add(future);
        }

        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
        System.out.println(ITest.list);
    }

    static class Test implements ITest { }

    interface ITest {
        List<Integer> list = new ArrayList<>();

        default void add(Integer num) {
            list.add(num);
        }
    }
}

By using the IDEA jclasslib Bytecode Viewer plug-in to view the bytecode, you can see that the attributes of the interface are public static final modified.

2

👇🏻 Here are the returned results and a java.util.ConcurrentModificationException error is bound to be reported.

6: [0, 9, 6]
1: [0, 9, 6, 7, 8, 4, 5, 3, 2, 1]
9: [0, 9, 6]
7: [0, 9, 6, 7]
2: [0, 9, 6, 7, 8, 4, 5, 3, 2, 1]
3: [0, 9, 6, 7, 8, 4, 5, 3]
Exception in thread "main" java.util.concurrent.CompletionException: java.util.ConcurrentModificationException
  at java.base/java.util.concurrent.CompletableFuture.encodeThrowable(CompletableFuture.java:315)
  at java.base/java.util.concurrent.CompletableFuture.completeThrowable(CompletableFuture.java:320)
  at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1770)
  at java.base/java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(ThreadPerTaskExecutor.java:314)
  at java.base/java.lang.VirtualThread.run(VirtualThread.java:329)
Caused by: java.util.ConcurrentModificationException
  at java.base/java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1096)
  at java.base/java.util.ArrayList$Itr.next(ArrayList.java:1050)
  at java.base/java.util.AbstractCollection.toString(AbstractCollection.java:458)
  at com.zh.next.test.InterfaceBugTest.lambda$main$0(InterfaceBugTest.java:16)
  at java.base/java.util.concurrent.CompletableFuture$AsyncSupply.run(CompletableFuture.java:1768)

This is very consistent with the thread-unsafe feature of ArrayList. By replacing ArrayList with CopyOnWriteArrayList, the concurrent modification exception can be resolved.

The iterators returned by this class's iterator and listIterator methods are fail-fast: if the list is structurally modified at any time after the iterator is created, in any way except through the iterator's own remove or add methods, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.

Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.

This comment explains the behavior of the iterators provided by the ArrayList class in Java (returned by the iterator() and listIterator(int) methods), in particular the so-called "fail-fast" mechanism.

Fail-Fast

  1. Definition: When a thread is traversing a list, if the list is structurally modified by another thread (adding, deleting, or replacing elements), in any way except through the iterator's own remove() or add() methods, the iterator will throw a ConcurrentModificationException. This behavior is called "fail-fast".
  2. Purpose: It is to avoid undefined behavior or data inconsistency in the case of concurrent modification. It ensures that the program fails immediately when a concurrent modification is detected, rather than in a non-deterministic behavior at an undetermined time in the future.
  3. Implementation Principle: Generally, iterators maintain an internal counter related to the container (called modCount). Whenever the container is modified, this counter will increase. Each time the iterator accesses the next element, it checks to see if this counter has changed since the last call. If there is a change, the ConcurrentModificationException is thrown.
  4. Limitation: Despite efforts being made to ensure fail-fast, it is impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. This is because other threads may have modified the set between two checks, causing the iterator to fail to detect these changes.
  5. Usage recommendations: You should not rely on ConcurrentModificationException for program correctness. Its main purpose is to help developers find potential concurrency issues during the testing phase. The correct approach is to use appropriate synchronization methods for shared resources in a multi-threaded environment, such as the synchronized keyword or explicit locks.

In summary, the "fail-fast" mechanism is a design pattern to improve the robustness and debuggability of programs in a multi-threaded environment, but it should not be regarded as a reliable concurrency control strategy.

// List<Integer> list = new ArrayList<>();
List<Integer> list = new CopyOnWriteArrayList<>();

If you want the post-task list of BaseResult to belong only to the instantiated SimpleResult, you can change BaseResult from an interface to a class and modify other parts accordingly.

3. Further Analysis

What if BaseResult must be an interface? First of all, we need to understand why we must have an interface.

Let's review the knowledge points again. The Java classes use single inheritance and multi-implementation interfaces, while the Java interface can inherit multiple interfaces.

Assume that, as shown in the following code, BaseResult inherits multiple interfaces, among which the IResult interface has an abstract method. At this point, if we change BaseResult from an interface to a class, we need to implement the abstract method. This may violate our original intention. In fact, we want the inherited upper class to implement it, not the base class. Although it is a way to let the upper class inherit BaseResult and implement IResult, our abstraction and encapsulation are not done well.

Therefore, in the case of the interface inheriting multiple interfaces, BaseResult may need to be an interface, rather than a class or an abstract class. In this case, it is recommended to put some instance attributes in the upper layer, which are not suitable for this interface.

public interface BaseResult extends Serializable, IResult {}
public interface IResult {
  String getResult();
}

4. Summary

Many problems are actually caused by very basic reasons. A simple line of code may also have different effects in different scenarios. Only by taking every line of code seriously and understanding the operation logic of each line can we truly understand it.

Finally, let's review the knowledge points:

l constant values defined in the interface are public, static, and final by default.

rayList is not thread-safe; for a thread-safe alternative, use CopyOnWriteArrayList.

terfaces can inherit multiple interfaces.

References

• Defining an Interface (The Java™ Tutorials > Learning the Java Language > Interfaces and Inheritance) (oracle.com): https://docs.oracle.com/javase/tutorial/java/IandI/interfaceDef.html

• Java Interfaces | Baeldung: https://www.baeldung.com/java-interfaces#overview

• jclasslib Bytecode Viewer: https://plugins.jetbrains.com/plugin/9248-jclasslib-bytecode-viewer


Disclaimer: The views expressed herein are for reference only and don't necessarily represent the official views of Alibaba Cloud.

0 1 0
Share on

Alibaba Cloud Community

993 posts | 242 followers

You may also like

Comments

Alibaba Cloud Community

993 posts | 242 followers

Related Products