今天帮同学做笔试题,在scanner的使用中遇到一个小坑,都怪自己学艺不精,赶紧来记录一下
问题来源
在线笔试题中,有时会给定多行输入,但是行数是未知的,这时该如何解决这一问题呢
容易想到但错误的解决方案
import java.util.Scanner;
public class test {
public static void main(String[] args)
{
Scanner sc = new Scanner(System.in);
String s;
while (sc.hasNext()){
System.out.println("loop");
if ((s = sc.nextLine()).equals(""))
break;
}
}
}
以上代码试图通过hasNext()方法解决这一问题,问题似乎是解决了,当没有输入的时候自动退出while不是很好吗?我们来看一下运行结果
注意看命令行中光标的位置,这是我点击了多次回车后的效果,可以看出,程序丝毫没有要退出while循环的迹象。
问题追踪
我们来看一下hasNext()方法的源码,注释中写的很清楚,这一方法很可能造成线程阻塞,也就是说如果方法发现缓冲区中没有数据,它就会一直等下去。观察源码,方法中第三行有一个while,聪明的同学可能会想,我们把sourceClosed改了,把数据源关闭是不是就可以了,答案也是否定的,因为线程在这个方法中已经阻塞了,没有多余的精力去分析现状并关闭数据源。
那到底该怎么解决这一问题呢?
往下看
解决方案
先放代码
import java.util.Scanner;
public class test {
public static void main(String[] args)
{
Scanner sc = new Scanner(System.in);
String s;
while (true){
System.out.println("loop");
if ((s = sc.nextLine()).equals(""))
break;
}
}
}
这一次就可以了,因为当线程不会在hasNext方法中阻塞了,一旦出现空行,我们可以自行读取,并退出循环。
那这样不判断hasNext直接读取数据,会不会造成异常呢?
我们来看下源码是怎么实现的
/**
* Advances this scanner past the current line and returns the input
* that was skipped.
*
* This method returns the rest of the current line, excluding any line
* separator at the end. The position is set to the beginning of the next
* line.
*
* <p>Since this method continues to search through the input looking
* for a line separator, it may buffer all of the input searching for
* the line to skip if no line separators are present.
*
* @return the line that was skipped
* @throws NoSuchElementException if no line was found
* @throws IllegalStateException if this scanner is closed
*/
public String nextLine() {
if (hasNextPattern == linePattern())
return getCachedResult();
clearCaches();
String result = findWithinHorizon(linePattern, 0);
if (result == null)
throw new NoSuchElementException("No line found");
MatchResult mr = this.match();
String lineSep = mr.group(1);
if (lineSep != null)
result = result.substring(0, result.length() - lineSep.length());
if (result == null)
throw new NoSuchElementException();
else
return result;
}
源码中写了
if (result == null)
throw new NoSuchElementException(“No line found”);
那难道不会造成异常吗?
我们看到这一行的上面调用了findWithinHorizon方法,看看这一方法是怎么实现的吧?
/**
* Attempts to find the next occurrence of the specified pattern.
*
* <p>This method searches through the input up to the specified
* search horizon, ignoring delimiters. If the pattern is found the
* scanner advances past the input that matched and returns the string
* that matched the pattern. If no such pattern is detected then the
* null is returned and the scanner's position remains unchanged. This
* method may block waiting for input that matches the pattern.
*
* <p>A scanner will never search more than <code>horizon</code> code
* points beyond its current position. Note that a match may be clipped
* by the horizon; that is, an arbitrary match result may have been
* different if the horizon had been larger. The scanner treats the
* horizon as a transparent, non-anchoring bound (see {@link
* Matcher#useTransparentBounds} and {@link Matcher#useAnchoringBounds}).
*
* <p>If horizon is <code>0</code>, then the horizon is ignored and
* this method continues to search through the input looking for the
* specified pattern without bound. In this case it may buffer all of
* the input searching for the pattern.
*
* <p>If horizon is negative, then an IllegalArgumentException is
* thrown.
*
* @param pattern the pattern to scan for
* @param horizon the search horizon
* @return the text that matched the specified pattern
* @throws IllegalStateException if this scanner is closed
* @throws IllegalArgumentException if horizon is negative
*/
public String findWithinHorizon(Pattern pattern, int horizon) {
ensureOpen();
if (pattern == null)
throw new NullPointerException();
if (horizon < 0)
throw new IllegalArgumentException("horizon < 0");
clearCaches();
// Search for the pattern
while (true) {
String token = findPatternInBuffer(pattern, horizon);
if (token != null) {
matchValid = true;
return token;
}
if (needInput)
readInput();
else
break; // up to end of input
}
return null;
}
可以看到,这一方法的实现中用了一个while(true)来读取缓冲区,如果读到数据才返回,所以直接读不会造成异常啦~
总结
采用本文中介绍的方法读取多行时,因为hasNext()方法仅仅将回车符作为结束符,所以当我们如果想停止输入,在最后一行直接点击回车就好了,这时nextLine()方法会返回一个空字符串(就是""
),程序就会通过break退出循环了。