Java source code - amazing Interviewer: from simple to deep

Posted by haaglin on Wed, 29 Sep 2021 20:42:02 +0200

No, let's start!

Guiding language

At present, many large factories require handwritten code during the interview. I once saw a large factory require online code during the interview. The topic is: handwritten the implementation of a queue without using the existing Java queue API, and the data structure of the queue, the way to join and leave the queue are defined by themselves.

In fact, this question examines several points:

  1. Check whether you are familiar with the internal structure of the queue;
  2. Examine your ability to define API s;
  3. Examine the basic skills of writing code and code style.

This chapter will work with you to write a queue based on the above points and get familiar with the idea and process. See demo.four.DIYQueue and demo.four.DIYQueueDemo for the complete queue code

1. Interface definition

Before implementing the queue, we first need to define the queue interface, which is often called API. API is the facade of our queue. The main principle of definition is simplicity and ease of use.

The queue we implemented this time only defines two functions: putting data and taking data. The interface is defined as follows:

/**
* Define the interface of the queue and define generic types, so that users can put any type into the queue
*/
public interface Queue<T> {
 
  /**
   * Release data
   * @param item Input parameter
   * @return true Success, false, failure
   */
  boolean put(T item);
 
  /**
   * Take the data and return a generic value
   * @return
   */
  T take();
 
  // The basic structure of the elements in the queue
  class Node<T> {
    // Data itself
    T item;
    // Next element
    Node<T> next;
 
    // constructor 
    public Node(T item) {
      this.item = item;
    }
  }
}

Here are a few points to explain:

  1. When defining an interface, you must write comments, interface comments, method comments, etc., so that it will be much easier for others to see our interface;

  2. When defining an interface, the name should be concise and clear. It's best to let others know what the interface does as soon as they see the name. For example, we name it Queue, and others can see that the interface is Queue related;

  3. Use generics well, because we don't know which values are put in the queue, so we use generic T, which means that any value can be put in the queue;

  4. There is no need to write public methods on the interface, because the methods in the interface are public by default, and the compiler will be grayed out if you write them;

  5. We define the basic element Node in the interface, so that the queue subclass can be used directly if you want to use it, increasing the possibility of reuse.

2. Queue subclass implementation

  Then we will start to write subclass implementation. We are going to write a queue of the most commonly used linked list data structure.

2.1 data structure

We use linked list as the underlying data structure. When it comes to linked list, you should immediately think of three key elements: chain header, chain tail and linked list elements. We have also implemented them. The code is as follows:

/**
 * Queue header
 */
private volatile Node<T> head;
 
/**
 * Queue tail
 */
private volatile Node<T> tail;
 
/**
 * Custom queue elements
 */
class DIYNode extends Node<T>{
  public DIYNode(T item) {
    super(item);
  }
}

In addition to these elements, we also have the capacity of the queue container, the current use size of the queue, placing data locks, taking data locks, etc. the code is as follows:

/**
 * The size of the queue, using AtomicInteger to ensure its thread safety
 */
private AtomicInteger size = new AtomicInteger(0);
 
/**
 * capacity
 */
private final Integer capacity;
 
/**
 * Release data lock
 */
private ReentrantLock putLock = new ReentrantLock();
 
/**
 * Take the data lock
 */
private ReentrantLock takeLock = new ReentrantLock();

2.2 initialization

We provide two methods: using the default capacity (the maximum value of Integer) and specifying the capacity. The code is as follows:

/**
 * No parameter constructor. The default maximum capacity is Integer.MAX_VALUE
 */
public DIYQueue() {
  capacity = Integer.MAX_VALUE;
  head = tail = new DIYNode(null);
}
 
/**
 * There is a parameter constructor, which can set the size of the capacity
 * @param capacity
 */
public DIYQueue(Integer capacity) {
  // Check the boundary
  if(null == capacity || capacity < 0){
    throw new IllegalArgumentException();
  }
  this.capacity = capacity;
  head = tail = new DIYNode(null);
}

2.3 implementation of put method

public boolean put(T item) {
  // Prohibit null data
  if(null == item){
    return false;
  }
  try{
    // When you try to add a lock, the lock is not obtained for 500 milliseconds and is directly interrupted
    boolean lockSuccess = putLock.tryLock(300, TimeUnit.MILLISECONDS);
	if(!lockSuccess){
	  return false;
	}
    // Verify queue size
    if(size.get() >= capacity){
      log.info("queue is full");
      return false;
    }
    // Append to end of line
    tail = tail.next = new DIYNode(item);
    // count
    size.incrementAndGet();
    return true;
  } catch (InterruptedException e){
    log.info("tryLock 500 timeOut", e);
    return false;
  } catch(Exception e){
    log.error("put error", e);
    return false;
  } finally {
    putLock.unlock();
  }
}

There are several points to note in the implementation of the put method:

  1. Pay attention to the rhythm of try catch finally. Catch can catch various types of exceptions. Here we catch timeout exceptions and unknown exceptions. In finally, remember to release the lock, otherwise the lock will not be released automatically. This must not be used incorrectly, which reflects the accuracy of our code;
  2. Necessary logic checks are still needed, such as null pointer check whether the input parameter is empty and critical check whether the queue is full. These check codes can reflect the tightness of our logic;
  3. It is also very important to add logs and comments to the key parts of the code. We do not want the key logic code to have no comments and logs, which is not conducive to reading the code and troubleshooting problems;
  4. Pay attention to thread safety. In addition to locking, for the size of capacity, we select the thread safe counting class: AtomicInteger to ensure thread safety;
  5. When locking, we'd better not use the method of forever blocking. We must use the blocking method with timeout. The timeout we set here is 300 milliseconds, that is, if the lock has not been obtained within 300 milliseconds, the put method directly returns false. Of course, the time can be set according to the situation;
  6. Set different return values according to different situations. The put method returns false. When an exception occurs, we can choose to return false or throw an exception directly;
  7. Put data is appended to the end of the queue, so we only need to convert the new data into DIYNode and put it at the end of the queue.

2.4 implementation of take method

The implementation of the take method is very similar to that of the put method, except that take takes data from the header. The code implementation is as follows:

public T take() {
  // If the queue is empty, null is returned
  if(size.get() == 0){
    return null;
  }
  try {
    // Take the data, we set a shorter timeout
    boolean lockSuccess = takeLock.tryLock(200,TimeUnit.MILLISECONDS);
	if(!lockSuccess){
	    throw new RuntimeException("Locking failed");
	}
    // Take out the next element of the header node
    Node expectHead = head.next;
    // Take out the value of the head node
    T result = head.item;
    // Set the value of the header node to null to help gc
    head.item = null;
    // Reset the value of the head node
    head = (DIYNode) expectHead;
    size.decrementAndGet();
    // Returns the value of the header node
    return result;
  } catch (InterruptedException e) {
    log.info(" tryLock 200 timeOut",e);
  } catch (Exception e) {
    log.info(" take error ",e);
  }finally {
      takeLock.unlock();
 }
  return null;
}

Through the above steps, our queue has been written. See demo.four.DIYQueue for the complete code.

3. Testing

After the API is written, we will write some scenario tests and unit tests for the API. First, we will write a scenario test to see if the API can run. The code is as follows:

@Slf4j
public class DIYQueueDemo {
	// We need to test the queue
  private final static Queue<String> queue = new DIYQueue<>();
	// producer
  class Product implements Runnable{
    private final String message;
 
    public Product(String message) {
      this.message = message;
    }
 
    @Override
    public void run() {
      try {
        boolean success = queue.put(message);
        if (success) {
          log.info("put {} success", message);
          return;
        }
        log.info("put {} fail", message);
      } catch (Exception e) {
        log.info("put {} fail", message);
      }
    }
  }
	// consumer
  class Consumer implements Runnable{
    @Override
    public void run() {
      try {
        String message = (String) queue.take();
        log.info("consumer message :{}",message);
      } catch (Exception e) {
        log.info("consumer message fail",e);
      }
    }
  }
	// Scenario test
  @Test
  public void testDIYQueue() throws InterruptedException {
    ThreadPoolExecutor executor =
        new ThreadPoolExecutor(10,10,0,TimeUnit.MILLISECONDS,
                               new LinkedBlockingQueue<>());
    for (int i = 0; i < 1000; i++) {
        // If it is even, it will be submitted to a producer, and if it is odd, it will be submitted to a consumer
        if(i % 2 == 0){
          executor.submit(new Product(i+""));
          continue;
        }
        executor.submit(new Consumer());
    }
    Thread.sleep(10000);
  }

The scenario of code test is relatively simple. It starts from 0 to 1000. If it is an even number, let the producer produce the data and put it in the queue. If it is an odd number, let the consumer take the data out of the queue for consumption. The results after running are as follows:

  From the displayed results, we can see that the DIYQueue we wrote has no big problem. Of course, if we want to use it on a large scale, we also need detailed unit tests and performance tests.

4. Summary

Through the study of this chapter, I don't know if you have a feeling that the queue is very simple. In fact, the queue itself is very simple and not as complex as expected.

As long as we understand the basic principle of queue and several common data structures, the problem of handwriting queue is not big. You should try it quickly.

No wordy, the end of the article, it is recommended to connect three times!

Topics: Java Interview