Java - 将文件扫描仪与用户输入扫描仪相结合
Java - combine Scanner for file with Scanner for user input
Edit2:请参阅下面的一些示例代码,这是留下来的,因为人们确实以原始形式回复了这个问题。
这是我和我的 CS 教授的合集:
我们有一项作业要求学生为 SQL 的修改子集编写基本命令界面。并不重要,但提供上下文。
要求说明命令可以在文件中或在命令提示符下输入。扫描仪对此似乎很自然,相当明显。
所以对于这个问题,我将引用我正在进行的学生项目的提交:
https://github.com/greysondn/fallcorp/tree/fe5f2a317ff3f3206e7dd318cb50f9f67519b02b
两个相关的类是net.darkglass.Appmanager and net.darkglass.arasql.command.ExecuteCommand。
问题出现在 AppManager 中的 ln 51 转发和 ExecuteCommand 中的 ln 56 转发的组合。 Scanner 管理用户输入的循环和 Scanner 管理逐行读取文件的循环不兼容;因此,我和我的教授都无法理解将 Scanner 的两种情况组合成一种方法的方法。
换句话说,这两种结构归根结底非常相似,应该是一模一样的,但我们找不到不比现在更糟的地方.
有没有一种方法可以编写扫描程序,使其既适用于用户输入又适用于用户输入的文件输入?
一些快速观察:
我在我的代码中注意到有些东西开始有点臭了;也就是说,事情直觉上是错误的。这发生在 ExecuteCommand 中,因为它是要写入的两个中的第二个。
此代码松散地遵循 Interpreter design pattern。我的母语更像 Pythonic and/or C++。一些成语和处事方式无疑会体现这一点。
我的教授很清楚我至少打算 post 这个问题,并且和我一样好奇和激动。当他解决这个项目以确保它是可行的以及需要多长时间时,他遇到了同样的绊脚石,找不到令他满意的解决方案。
编辑:上下文很重要;请在指定的位置打开这两个文件并稍微检查一下。现在最终发生的情况是,两种扫描仪几乎相同,但由于两种类型 I/O 在 [=63= 中的工作方式,一种仅适用于文件,另一种仅适用于用户输入].从表面上看,我突然意识到这个问题听起来可能比实际情况要密集得多。 (是的,无论来源如何,使用扫描仪的任何方法都可以解析字符串,但这里的情况更多是关于在两个不同的来源上使用相同的扫描仪,这主要是由于它的使用方式)。
Edit2:经过一些评论,这里是演示核心问题的某种形式的代码。
public void doFile()
{
// set scanner up against some URI; this is messy but it's a
// "point of the matter" thing
Scanner cin = new Scanner(aFile);
// read over file
while (cin.hasNextLine())
{
// this is actually a lot more complicated, but ultimately we're
// just doing whatever the next line says
doWhatItSays(cin.nextLine());
}
}
public void doREPL()
{
// set scanner up against user input - this is the actual line
Scanner cin = new Scanner(System.in);
Boolean continueRunning = true;
while(continueRunning)
{
// pretty print prompt
System.out.println("");
System.out.print("$> ");
// This, like before, is a lot more complicated, but ultimately
// we just do whatever it says. (One of the things it may say
// to do is to set continueRunning to false.)
doWhatItSays(cin.nextLine());
}
}
他们都只是简单地扫描一个输入并按照它说的去做;将这两种方法合并为一种方法需要什么? (是的,它快速而混乱;它至少得到了重点和基本评论。)
我认为this will solve your problem。这基本上是 StdIn
class 摘自普林斯顿大学 算法,第 4 版 ,作者是 Robert Sedgewick 和 Kevin Wayne。我在他们的 Coursera MOOC 算法第一部分 中使用了这个 class 从文件或命令中读取输入。
编辑
结合上面提到的classwith In
class from the same repository。
编辑 2
在 In
class 你有两种方法:
/**
* Initializes an input stream from a file.
*
* @param file the file
* @throws IllegalArgumentException if cannot open {@code file}
* @throws IllegalArgumentException if {@code file} is {@code null}
*/
public In(File file) {
if (file == null) throw new IllegalArgumentException("file argument is null");
try {
// for consistency with StdIn, wrap with BufferedInputStream instead of use
// file as argument to Scanner
FileInputStream fis = new FileInputStream(file);
scanner = new Scanner(new BufferedInputStream(fis), CHARSET_NAME);
scanner.useLocale(LOCALE);
}
catch (IOException ioe) {
throw new IllegalArgumentException("Could not open " + file, ioe);
}
}
和
/**
* Initializes an input stream from a given {@link Scanner} source; use with
* {@code new Scanner(String)} to read from a string.
* <p>
* Note that this does not create a defensive copy, so the
* scanner will be mutated as you read on.
*
* @param scanner the scanner
* @throws IllegalArgumentException if {@code scanner} is {@code null}
*/
public In(Scanner scanner) {
if (scanner == null) throw new IllegalArgumentException("scanner argument is null");
this.scanner = scanner;
}
你根据用户输入调用正确的方法(假设你检查第一个参数是否是一个文件,这很容易做到)然后你有很多其他方法,比如 hasNextLine
或 readChar
。这一切都合而为一 class。无论您将使用哪个构造函数,都可以调用相同的方法。
我会把输入从主要代码中分离出来,所以你有一个接口 Source
轮询一个命令,然后有 2 个具体实现。
FileSource
只是从文件中读取
InteractiveSource
向用户显示提示和 return 他们 return 的文本
您可以将其中之一传递给您的 class,无论它从何处获取命令,它都以相同的方式使用它。
这样做的好处是,如果您想添加 NetworkSource
或 UISource
,您只需要编写一个新的 Source
实现即可。
这种方式还可以让您干净地关闭系统,因为每个 Source 都可以有一个方法 keepGoing()
,您可以在命令之间调用该方法来查看系统是否应该关闭,对于文件来说,它会在什么时候关闭没有更多的行,输入可能是在他们输入 exit
之后
看来你把问题想的太多了Scanner
,这与问题无关。如果你有匹配到 99% 的代码,直接的解决方案是将通用代码单独移动到一个方法中,并保留两个小的专用方法:
public void doFile() {
try(Scanner cin = new Scanner(aFile)) {
// read over file
while (cin.hasNextLine()) {
commonLoopBody(cin);
}
}
}
public void doREPL() {
// set scanner up against user input - this is the actual line
Scanner cin = new Scanner(System.in);
boolean continueRunning = true;
while(continueRunning) {
// pretty print prompt
System.out.printf("%n$> ");
commonLoopBody(cin);
}
}
private void commonLoopBody(Scanner cin) {
// this is actually a lot more complicated, but ultimately we're
// just doing whatever the next line says
doWhatItSays(cin.nextLine());
}
专用方法仍然包含循环语句,但这并没有错,因为循环 不同。
不过,还有一种方法是将差异从原始代码中移出,而不是从通用代码中移出,例如
public void doFile() {
try(Scanner cin = new Scanner(aFile)) {
commonLoop(cin, cin::hasNextLine, ()->{});
}
}
public void doREPL() {
boolean continueRunning = true;
commonLoop(new Scanner(System.in),()->continueRunning,()->System.out.printf("%n$> "));
}
private void commonLoop(Scanner cin, BooleanSupplier runCondition, Runnable beforeCommand){
while(runCondition.getAsBoolean()) {
beforeCommand.run();
// this is actually a lot more complicated, but ultimately we're
// just doing whatever the next line says
doWhatItSays(cin.nextLine());
}
}
在这个具体的例子中,将循环语句移动到公共代码中没有任何优势,但在某些情况下,在框架中进行循环维护会提供优势,Stream
API 是举个例子……
也就是说,查看您的具体代码,似乎存在根本性的误解。在 Java 中,String
对象是不可变的,在 String
上调用 concat
会创建一个新实例。因此,带有 String test = "";
这样的初始化程序的变量声明确实 而不是 “为事物预先保留了 space”,它通过使用引用初始化变量来浪费资源到一个空字符串,无论如何都会被随后的 test = cin.nextLine();
覆盖。此外,test = test.concat(" "); test = test.concat(cin.nextLine());
不必要地创建中间字符串实例,其中更简单的 test = test + " " + cin.nextLine();
使用构建器免费编译为代码。
但最终,如果您不再忽视 Scanner
的强大功能,这些操作就会过时。 class 不仅仅是提供现有 BufferedReader.readLine()
功能的另一种方式,它是一种模式匹配工具,允许在流输入上使用正则表达式引擎。
如果您想用分号分隔命令,请使用分号作为分隔符,而不是读取必须手动连接的行。替换多行命令的换行符和删除注释也可以通过单个模式替换操作来完成。例如
static String EXAMPLE_INPUT =
"a single line command;\n"
+ "-- a standalone comment\n"
+ "a multi\n"
+ "line\n"
+ "-- embedded comment\n"
+ "command;\n"
+ "multi -- line\n"
+ "command with double minus;\n"
+ "and just a last command;";
public static void main(String[] args) {
Scanner s = new Scanner(EXAMPLE_INPUT).useDelimiter(";(\R|\Z)");
while(s.hasNext()) {
String command = s.next().replaceAll("(?m:^--.*)?+(\R|\Z)", " ");
System.out.println("Command: "+command);
}
}
将打印
Command: a single line command
Command: a multi line command
Command: multi -- line command with double minus
Command: and just a last command
如果要保留结果中的分号,可以将分隔符从
";(\R|\Z)"
到 "(?<=;)(\R|\Z)"
.
Edit2:请参阅下面的一些示例代码,这是留下来的,因为人们确实以原始形式回复了这个问题。
这是我和我的 CS 教授的合集:
我们有一项作业要求学生为 SQL 的修改子集编写基本命令界面。并不重要,但提供上下文。
要求说明命令可以在文件中或在命令提示符下输入。扫描仪对此似乎很自然,相当明显。
所以对于这个问题,我将引用我正在进行的学生项目的提交: https://github.com/greysondn/fallcorp/tree/fe5f2a317ff3f3206e7dd318cb50f9f67519b02b
两个相关的类是net.darkglass.Appmanager and net.darkglass.arasql.command.ExecuteCommand。
问题出现在 AppManager 中的 ln 51 转发和 ExecuteCommand 中的 ln 56 转发的组合。 Scanner 管理用户输入的循环和 Scanner 管理逐行读取文件的循环不兼容;因此,我和我的教授都无法理解将 Scanner 的两种情况组合成一种方法的方法。
换句话说,这两种结构归根结底非常相似,应该是一模一样的,但我们找不到不比现在更糟的地方.
有没有一种方法可以编写扫描程序,使其既适用于用户输入又适用于用户输入的文件输入?
一些快速观察:
我在我的代码中注意到有些东西开始有点臭了;也就是说,事情直觉上是错误的。这发生在 ExecuteCommand 中,因为它是要写入的两个中的第二个。
此代码松散地遵循 Interpreter design pattern。我的母语更像 Pythonic and/or C++。一些成语和处事方式无疑会体现这一点。
我的教授很清楚我至少打算 post 这个问题,并且和我一样好奇和激动。当他解决这个项目以确保它是可行的以及需要多长时间时,他遇到了同样的绊脚石,找不到令他满意的解决方案。
编辑:上下文很重要;请在指定的位置打开这两个文件并稍微检查一下。现在最终发生的情况是,两种扫描仪几乎相同,但由于两种类型 I/O 在 [=63= 中的工作方式,一种仅适用于文件,另一种仅适用于用户输入].从表面上看,我突然意识到这个问题听起来可能比实际情况要密集得多。 (是的,无论来源如何,使用扫描仪的任何方法都可以解析字符串,但这里的情况更多是关于在两个不同的来源上使用相同的扫描仪,这主要是由于它的使用方式)。
Edit2:经过一些评论,这里是演示核心问题的某种形式的代码。
public void doFile()
{
// set scanner up against some URI; this is messy but it's a
// "point of the matter" thing
Scanner cin = new Scanner(aFile);
// read over file
while (cin.hasNextLine())
{
// this is actually a lot more complicated, but ultimately we're
// just doing whatever the next line says
doWhatItSays(cin.nextLine());
}
}
public void doREPL()
{
// set scanner up against user input - this is the actual line
Scanner cin = new Scanner(System.in);
Boolean continueRunning = true;
while(continueRunning)
{
// pretty print prompt
System.out.println("");
System.out.print("$> ");
// This, like before, is a lot more complicated, but ultimately
// we just do whatever it says. (One of the things it may say
// to do is to set continueRunning to false.)
doWhatItSays(cin.nextLine());
}
}
他们都只是简单地扫描一个输入并按照它说的去做;将这两种方法合并为一种方法需要什么? (是的,它快速而混乱;它至少得到了重点和基本评论。)
我认为this will solve your problem。这基本上是 StdIn
class 摘自普林斯顿大学 算法,第 4 版 ,作者是 Robert Sedgewick 和 Kevin Wayne。我在他们的 Coursera MOOC 算法第一部分 中使用了这个 class 从文件或命令中读取输入。
编辑
结合上面提到的classwith In
class from the same repository。
编辑 2
在 In
class 你有两种方法:
/**
* Initializes an input stream from a file.
*
* @param file the file
* @throws IllegalArgumentException if cannot open {@code file}
* @throws IllegalArgumentException if {@code file} is {@code null}
*/
public In(File file) {
if (file == null) throw new IllegalArgumentException("file argument is null");
try {
// for consistency with StdIn, wrap with BufferedInputStream instead of use
// file as argument to Scanner
FileInputStream fis = new FileInputStream(file);
scanner = new Scanner(new BufferedInputStream(fis), CHARSET_NAME);
scanner.useLocale(LOCALE);
}
catch (IOException ioe) {
throw new IllegalArgumentException("Could not open " + file, ioe);
}
}
和
/**
* Initializes an input stream from a given {@link Scanner} source; use with
* {@code new Scanner(String)} to read from a string.
* <p>
* Note that this does not create a defensive copy, so the
* scanner will be mutated as you read on.
*
* @param scanner the scanner
* @throws IllegalArgumentException if {@code scanner} is {@code null}
*/
public In(Scanner scanner) {
if (scanner == null) throw new IllegalArgumentException("scanner argument is null");
this.scanner = scanner;
}
你根据用户输入调用正确的方法(假设你检查第一个参数是否是一个文件,这很容易做到)然后你有很多其他方法,比如 hasNextLine
或 readChar
。这一切都合而为一 class。无论您将使用哪个构造函数,都可以调用相同的方法。
我会把输入从主要代码中分离出来,所以你有一个接口 Source
轮询一个命令,然后有 2 个具体实现。
FileSource
只是从文件中读取InteractiveSource
向用户显示提示和 return 他们 return 的文本
您可以将其中之一传递给您的 class,无论它从何处获取命令,它都以相同的方式使用它。
这样做的好处是,如果您想添加 NetworkSource
或 UISource
,您只需要编写一个新的 Source
实现即可。
这种方式还可以让您干净地关闭系统,因为每个 Source 都可以有一个方法 keepGoing()
,您可以在命令之间调用该方法来查看系统是否应该关闭,对于文件来说,它会在什么时候关闭没有更多的行,输入可能是在他们输入 exit
看来你把问题想的太多了Scanner
,这与问题无关。如果你有匹配到 99% 的代码,直接的解决方案是将通用代码单独移动到一个方法中,并保留两个小的专用方法:
public void doFile() {
try(Scanner cin = new Scanner(aFile)) {
// read over file
while (cin.hasNextLine()) {
commonLoopBody(cin);
}
}
}
public void doREPL() {
// set scanner up against user input - this is the actual line
Scanner cin = new Scanner(System.in);
boolean continueRunning = true;
while(continueRunning) {
// pretty print prompt
System.out.printf("%n$> ");
commonLoopBody(cin);
}
}
private void commonLoopBody(Scanner cin) {
// this is actually a lot more complicated, but ultimately we're
// just doing whatever the next line says
doWhatItSays(cin.nextLine());
}
专用方法仍然包含循环语句,但这并没有错,因为循环 不同。
不过,还有一种方法是将差异从原始代码中移出,而不是从通用代码中移出,例如
public void doFile() {
try(Scanner cin = new Scanner(aFile)) {
commonLoop(cin, cin::hasNextLine, ()->{});
}
}
public void doREPL() {
boolean continueRunning = true;
commonLoop(new Scanner(System.in),()->continueRunning,()->System.out.printf("%n$> "));
}
private void commonLoop(Scanner cin, BooleanSupplier runCondition, Runnable beforeCommand){
while(runCondition.getAsBoolean()) {
beforeCommand.run();
// this is actually a lot more complicated, but ultimately we're
// just doing whatever the next line says
doWhatItSays(cin.nextLine());
}
}
在这个具体的例子中,将循环语句移动到公共代码中没有任何优势,但在某些情况下,在框架中进行循环维护会提供优势,Stream
API 是举个例子……
也就是说,查看您的具体代码,似乎存在根本性的误解。在 Java 中,String
对象是不可变的,在 String
上调用 concat
会创建一个新实例。因此,带有 String test = "";
这样的初始化程序的变量声明确实 而不是 “为事物预先保留了 space”,它通过使用引用初始化变量来浪费资源到一个空字符串,无论如何都会被随后的 test = cin.nextLine();
覆盖。此外,test = test.concat(" "); test = test.concat(cin.nextLine());
不必要地创建中间字符串实例,其中更简单的 test = test + " " + cin.nextLine();
使用构建器免费编译为代码。
但最终,如果您不再忽视 Scanner
的强大功能,这些操作就会过时。 class 不仅仅是提供现有 BufferedReader.readLine()
功能的另一种方式,它是一种模式匹配工具,允许在流输入上使用正则表达式引擎。
如果您想用分号分隔命令,请使用分号作为分隔符,而不是读取必须手动连接的行。替换多行命令的换行符和删除注释也可以通过单个模式替换操作来完成。例如
static String EXAMPLE_INPUT =
"a single line command;\n"
+ "-- a standalone comment\n"
+ "a multi\n"
+ "line\n"
+ "-- embedded comment\n"
+ "command;\n"
+ "multi -- line\n"
+ "command with double minus;\n"
+ "and just a last command;";
public static void main(String[] args) {
Scanner s = new Scanner(EXAMPLE_INPUT).useDelimiter(";(\R|\Z)");
while(s.hasNext()) {
String command = s.next().replaceAll("(?m:^--.*)?+(\R|\Z)", " ");
System.out.println("Command: "+command);
}
}
将打印
Command: a single line command
Command: a multi line command
Command: multi -- line command with double minus
Command: and just a last command
如果要保留结果中的分号,可以将分隔符从
";(\R|\Z)"
到 "(?<=;)(\R|\Z)"
.