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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章