java.util.NoSuchElementException

遇到的問題–java.util.NoSuchElementException

在測試程序時,遇到了java.util.NoSuchElementException,發現原因是程序中的Scanner使用的System.in這個System類**靜態變量**InputStream(這點倒是一直忽視了)導致的。

The “standard” input stream. This stream is already open and ready to supply input data. Typically this stream corresponds to keyboard input or another input source specified by the host environment or user.

在我的程序中,有代碼如下

     public void remove(){   
         java.util.Scanner input = new java.util.Scanner(System.in);  
         ...... 
         input.close();  
     }  
     public void opearate(){    
        java.util.Scanner input = new java.util.Scanner(System.in);  
        ......  
        remove();  
        input.nextInt();
        ......  
        input.close();   
     }   

opearateromove都用System.in創建了Scanner,當opearateScanner還沒有關閉,調用了remove,而remove中則是打開Scanner後關閉,這裏間接地將System.in也關閉了,因爲System.in是個靜態流,所以引用其的Scanner對象內都是相同狀態的。當操作後返回到opearate,這時再調用nextInt,在System.in已經關閉了情況下,不能讀取到任何數據,就會產生 java.util.NoSuchElementException,即使是在兩個不同的Scanner對象中調用System.in(兩者hash值不一樣)。

但是一般Scanner關閉後再讀取應該拋出的是java.lang.IllegalStateException: Scanner closed(這纔是常見的)纔對。那麼爲什麼拋出的是NoSuchElementException,兩者的區別在哪裏?

在我的程序中,涉及到是Scanner$close()和Scanner$input()。首先先來了解Scanner$close()都幹了些什麼?

    public void close() {
        if (closed)                             //1.通過closed標誌校驗Scanner是否以關閉;
            return;
        if (source instanceof Closeable) {      //2.執行source的close()方法,
            try {                               //將source關閉(這裏爲System.in);
                ((Closeable)source).close();
            } catch (IOException ioe) {
                lastException = ioe;
            }
        }
        sourceClosed = true;    //3.將sourceClosed標誌設置爲true,表示source已關閉;
        source = null;          //4.將source置爲null,不再引用,處於可回收狀態;
        closed = true;          //5.將closed標誌設置爲true,表示Scanner已關閉;
    }

我們可以知道當調用Scanner$close()時,source會進入已經關閉且沒引用任何對象的狀態。

再看回Scanner$nextInt(),nextInt()實際以nextInt()->nextInt(10)->next(Pattern)的順序調用next(Pattern)的。首先要對Scanner的一些靜態變量有所瞭解,

變量名 作用
buf 緩存區,保存從source讀取的數據
position 當前buf已讀取到的最大下標,也是下一次從buf中讀取的起始位置
needInput 是否需要從source中讀取數據到buf
source Scanner的輸入源,獲取數據的地方,本文爲System.in
sourceClosed 代表source是否關閉
closed 代表Scanner對象是否關閉

然後看next(Pattern)的源代碼,整理可得:

步驟 調用的方法或關鍵語句 作用 影響的變量
next(Pattern) ensureOpen() 校驗Scanner對象是否關閉
getCompleteTokenInBuffer(Pattern)
readInput() 當needInput爲true時調用,從source讀取數據到buf
throwFor()
ensureOpen() 當Scanner對象已關閉時拋出異常IllegalStateException(“Scanner closed”) closed
getCompleteTokenInBuffer(Pattern) 首先通過delimPattern匹配buf,完成匹配則代表需要從source讀取數據,將needInput設置爲true並返回,否則使用入參Pattern匹配buf,獲取token,並設置position buf,needInput,sourceClosed,position
readInput() source.read(buf) 從source處讀取數據到buf,若source.read(buf)返回-1則表示無法讀取,sourceClosed設置爲true, buf,needInput,sourceClosed,position,
throwFor() 當source已關閉,且無法從buf讀取數據時,拋出異常NoSuchElementException,否則拋出異常InputMismatchException

其中的delimPattern默認是WHITESPACE_PATTERN,即\p{javaWhitespace},這等效於 java.lang.Character.isWhitespace(),可參考:Java正則表達式進階教程之構造方法

瞭解其中的大概後,再來看程序的執行順序。一般執行的順序是getCompleteTokenInBuffer(Pattern)->readInput()->getCompleteTokenInBuffer(Pattern)->返回token,而我遇到的NoSuchElementException則是getCompleteTokenInBuffer(Pattern)->readInput()->getCompleteTokenInBuffer(Pattern)->throwFor(),其中的關鍵在於getCompleteTokenInBuffer(Pattern)的處理,我們細看兩者的流程。

步驟 正常情況 拋出NoSuchElementException
getCompleteTokenInBuffer(Pattern) 執行到position == buf.limit()爲true,表示buf中沒有數據,需要從source中讀取,執行needInput = true; 同左
readInput() 從source中讀取token並保存到buf(阻塞等待),執行needInput = false; 無法從source讀取數據,執行sourceClosed = true;needInput = false;
getCompleteTokenInBuffer(Pattern) 通過delimPattern找到開始讀取的buf下標,並通過參數Pattern讀取符合條件的token,然後返回token到最初的調用者,如果token不完整,則再次調用readInput(),重複步驟1、2 執行到position == buf.limit()後返回null,到next(Pattern),因爲needInput爲true,調用throwFor()
throwFor() 不調用,此時已返回token到最初的調用者,此處爲remove方法和opearate 拋出異常NoSuchElementException

注意當從remove回到operate時,operate本身的Scanner對象的sourceClosed 爲false,通過readInput()從source讀取失敗後(拋錯java.io.IOException: Stream closed)才設置爲true的。

回到最初的問題,現在可以很肯定地回答由於靜態流System.in的關閉,導致無法從source中讀取數據,且嘗試讀取時程序中才會調用throwFor方法拋出NoSuchElementException異常,而IllegalStateException是在這之前由ensureOpen()拋出的。

最後附上所需的源代碼供閱讀,手動複製~

public String next(Pattern pattern) {
        ensureOpen();
        ...
        clearCaches();

        // Search for the pattern
        while (true) {
            String token = getCompleteTokenInBuffer(pattern);
            ...
            if (needInput)
                readInput();
            else
                throwFor();
        }
    }

    // If we are at the end of input then NoSuchElement;
    // If there is still input left then InputMismatch
    private void throwFor() {
        skipped = false;
        if ((sourceClosed) && (position == buf.limit()))
            throw new NoSuchElementException();
        else
            throw new InputMismatchException();
    }

    // Tries to read more input. May block.
    private void readInput() {
        if (buf.limit() == buf.capacity())
            makeSpace();

        // Prepare to receive data
        int p = buf.position();
        buf.position(buf.limit());
        buf.limit(buf.capacity());

        int n = 0;
        try {
            n = source.read(buf);
        } catch (IOException ioe) {
            lastException = ioe;
            n = -1;
        }

        if (n == -1) {
            sourceClosed = true;
            needInput = false;
        }

        if (n > 0)
            needInput = false;

        // Restore current position and limit for reading
        buf.limit(buf.position());
        buf.position(p);
    }

    private String getCompleteTokenInBuffer(Pattern pattern) {
        matchValid = false;

        // Skip delims first
        matcher.usePattern(delimPattern);
        if (!skipped) { // Enforcing only one skip of leading delims
            matcher.region(position, buf.limit());
            if (matcher.lookingAt()) {
                // If more input could extend the delimiters then we must wait
                // for more input
                if (matcher.hitEnd() && !sourceClosed) {
                    needInput = true;
                    return null;
                }
                // The delims were whole and the matcher should skip them
                skipped = true;
                position = matcher.end();
            }
        }

        // If we are sitting at the end, no more tokens in buffer
        if (position == buf.limit()) {
            if (sourceClosed)
                return null;
            needInput = true;
            return null;
        }

        // Must look for next delims. Simply attempting to match the
        // pattern at this point may find a match but it might not be
        // the first longest match because of missing input, or it might
        // match a partial token instead of the whole thing.

        // Then look for next delims
        matcher.region(position, buf.limit());
        boolean foundNextDelim = matcher.find();
        if (foundNextDelim && (matcher.end() == position)) {
            // Zero length delimiter match; we should find the next one
            // using the automatic advance past a zero length match;
            // Otherwise we have just found the same one we just skipped
            foundNextDelim = matcher.find();
        }
        if (foundNextDelim) {
            // In the rare case that more input could cause the match
            // to be lost and there is more input coming we must wait
            // for more input. Note that hitting the end is okay as long
            // as the match cannot go away. It is the beginning of the
            // next delims we want to be sure about, we don't care if
            // they potentially extend further.
            if (matcher.requireEnd() && !sourceClosed) {
                needInput = true;
                return null;
            }
            int tokenEnd = matcher.start();
            // There is a complete token.
            if (pattern == null) {
                // Must continue with match to provide valid MatchResult
                pattern = FIND_ANY_PATTERN;
            }
            //  Attempt to match against the desired pattern
            matcher.usePattern(pattern);
            matcher.region(position, tokenEnd);
            if (matcher.matches()) {
                String s = matcher.group();
                position = matcher.end();
                return s;
            } else { // Complete token but it does not match
                return null;
            }
        }

        // If we can't find the next delims but no more input is coming,
        // then we can treat the remainder as a whole token
        if (sourceClosed) {
            if (pattern == null) {
                // Must continue with match to provide valid MatchResult
                pattern = FIND_ANY_PATTERN;
            }
            // Last token; Match the pattern here or throw
            matcher.usePattern(pattern);
            matcher.region(position, buf.limit());
            if (matcher.matches()) {
                String s = matcher.group();
                position = matcher.end();
                return s;
            }
            // Last piece does not match
            return null;
        }

        // There is a partial token in the buffer; must read more
        // to complete it
        needInput = true;
        return null;
    }

    // Throws if the scanner is closed
    private void ensureOpen() {
        if (closed)
            throw new IllegalStateException("Scanner closed");
    }
發佈了49 篇原創文章 · 獲贊 16 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章