Write a BUG again

In my last book, I wrote a book calledThis is probably the most elegant writing you’ve ever seen”The method is to decouple the non-business logic from the business logic, so the friends around feel good and start to use it, which makes people feel very successful. So everyone started using it. Now that I’ve said that, you already know that there’s a bug going on.

The beginning of the story

One day, a friend of mine noticed that a dubbo interface for querying goods reported NoSuchElementException, and then I saw a line of the same public method I had provided in kibana’s stack logSo let’s see what row is this

Common NoSuchElementException causes

Does NoSuchElementException look familiar? Yes, if you’re interested, you’ll find a lot of these errors on the web, just because next() was used multiple times in the loop, for example:

List<String> list = new ArrayList<>();
list.add("wo");
list.add("ni");
list.add("ta");
System.out.println(list);
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
     iterator.next();
     System.out.println(iterator.next());
}
Copy the code

Take a closer look at line 862 of the ArrayList (I use JDK version 1.8.181, the number of lines may vary from version to version)You can see that the cursor count is checked here when the next() method is executed. As you can see, in the example above, the cursor moves twice in each for loop due to executing next() once more. You can DEBUG it yourself.

Return to the main line

So, the reason was the place of too, due to company policy requirements, my side is not convenient to stick out the relevant business code (this is to be removed), but there didn’t use the iterator loop method and enhanced but with a simple for loop, also won’t appear like the above many times call next (). So we had to simulate it locally:

public static void main(String[] args) {
    List<String> all = Lists.newArrayList("ki1");
    Map<String, Object> map = new CommonDoPartition<>().partitionToQuery4Map(500, all, outerIds -> returnMap(outerIds));
    System.out.println(JSON.toJSONString(map));
}

public static Map<String, Object> returnMap(List<String> all) {
    Map<String, Object> map = Maps.newHashMap();
    map.put("ki1"."ki1");
    return map;
}
Copy the code

After running, it is found that no error is reported.This is really freaking Ben out.In the code that failed, a business method was also called in a lambda expression. The other parts are completely consistent, so according to the control variable method (of course, the running machine, container, environment and so on are not considered for the time being), is it the business code that is the problem? So I took a look at the business logic here. Sure enough, there are hints. In the business code, there is a truncation of the all parameter, so I also run a little test:

public static void main(String[] args) {
    List<String> all = Lists.newArrayList("ki1");
    Map<String, Object> map = new CommonDoPartition<>().partitionToQuery4Map(500, all, outerIds -> returnMap(outerIds));
    System.out.println(JSON.toJSONString(map));
}

public static Map<String, Object> returnMap(List<String> all) {
    Map<String, Object> map = Maps.newHashMap();
    all.remove("ki1");
    map.put("ki1"."ki1");
    return map;
}
Copy the code

Sure enough, this error was reported, which is the same as the error on Kibana.So let’s take a closer look at the code and why this error occurs.

Enhanced for loop

Enhanced for loops are common in everyday writing, when we don’t care about sequences in a list and just iterate through objects one by one. Of course, this is just the JVM level to wrap it into a fixed way of writing, in the specific interpretation, still according to the iterator flow. For example, a simple code like this:

List<Integer> list = Lists.newArrayList(1.2.3);
for (Integer i : list) {
    System.out.println(i);
}
Copy the code

After the javap explanation, capture some of the imagesAs you can see, iterator writing is used to iterate over the object, and the next() method is used to retrieve the object.

Back to the main line

But why talk about these things?If we look at the error message, we can see that the last line of the stack is stuck at

Exception in thread "main" java.util.NoSuchElementException at java.util.AbstractList$Itr.next(AbstractList.java:364) at  com.example.demo.consume.CommonDoPartition.partitionToQuery4Map(CommonDoPartition.java:67) at com.example.demo.consume.CommonDoPartition.main(CommonDoPartition.java:78)Copy the code

Look at line 364 of AbstractListAs you can see, the reported here because IndexOutOfBoundsException abnormalities, and then throws NoSuchElementException anomalies. Instead of the usual NoSuchElementException error (the code in Example 1), the next() method from the inner class is used.

So since this error will be reported, the problem is the get method in here. The get method here is an abstract class

    / * * * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc} * /
    abstract public E get(int index);
Copy the code

So we can use the Diagrams of IDEA and we can look at inheritanceTake a look at the implementation of GET

    @Override public List<T> get(int index) {
      checkElementIndex(index, size());// Perform a security check
      int start = index * size;
      int end = Math.min(start + size, list.size());
      return list.subList(start, end);// Use subList to intercept some data
    }
    
    // Get the size, which is actually rounded up
    @Override public int size(a) {
      return IntMath.divide(list.size(), size, RoundingMode.CEILING);
    }
Copy the code

So is also due to checkElementIndex IndexOutOfBoundsException conditions caused by this method is thrown out

  public static int checkElementIndex(
      int index, int size, @Nullable String desc) {
    // Carefully optimized for execution by hotspot (explanatory comment above)
    if (index < 0 || index >= size) {
      throw new IndexOutOfBoundsException(badElementIndex(index, size, desc));
    }
    return index;
  }
Copy the code

See here is not suddenly understand! Partition in Guava is an interception of the entire list of data, which is also a kind of pagination. Therefore, if the whole data is deleted in the subsequent business code, the value of size() here will not be correct, resulting in the data of the whole GET will not be the predicted value.

conclusion

So let’s summarize the process here:

  1. The enhanced for loop is interpreted as hasNext() and next() iterating over;
  2. The get() method in next() is actually a data interception of a list of elements, which can be understood as pagination;
  3. The get() method performs array out-of-bounds checks. When the list is truncated, the size() method does not return a fixed value.
  4. Each iteration will cursor cursor + 1, when the list data cuts, calling get () an array IndexOutOfBoundsException exception occurs, NoSuchElementException is then thrown in the next() method.

To solve

There are two ways to solve the problem:

  1. The control business invocation level disables the deletion of the original data list. (Although this is difficult to control due to the complexity of business scenarios, it is not recommended)
  2. In this scenario, instead of using Guava’s Partition, rewrite a data separation utility class yourself.


public class SubListIterator implements Iterator {

/** * private List<T> dataList; /** * private int subSize; private volatile int nextIndex = 0; private int listSize; private List<List<T>> subLists = new ArrayList<>(); public SubListIterator(List<T> dataList, int subSize){ if(dataList ! = null){ listSize = dataList.size(); } this.dataList = dataList; this.subSize = subSize; initSubLists(); } private void initSubLists(){ if(listSize <= 0){ return; } int index = 1; Iterator<T> iterator = dataList.iterator(); List<T> subDataList = new ArrayList(); while(iterator.hasNext()){ T next = iterator.next(); subDataList.add(next); if (index % subSize == 0 || listSize == index) { subLists.add(subDataList); subDataList = new ArrayList(); } index++; } } @Override public boolean hasNext() { return subLists.size() > nextIndex; } @Override public Object next() { if(hasNext()) { return subLists.get(nextIndex++); } return null; {} @ Override public void the remove () new UnsupportedOperationException (" does not support this operation "); }Copy the code

}

Here also attach my lot address: https://github.com/showyool/juejin.git # # finally thank you can see, this is above all the process I will handle this bug. $$\color{red}{{red}{$$\color{red}{{like}$ < br / > [🏆 nuggets technical essay - double festival special article] (https://juejin.cn/post/6878211126371287053)Copy the code