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:
- Check whether you are familiar with the internal structure of the queue;
- Examine your ability to define API s;
- 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:
-
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;
-
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;
-
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;
-
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;
-
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:
- 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;
- 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;
- 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;
- 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;
- 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;
- 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;
- 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!