问题
Edit2: See below for some example code, this is left because people did reply to this question in its original form.
Here's one from myself and my CS professor combined:
We have an assignment where students are being asked to write a basic command interface for a modified subset of SQL. Doesn't really matter, but provides context.
The requirements state that commands can be in a file or entered at a command prompt. Scanner seems natural for this, fairly obviously.
So for this question, I'll be referencing this commit of my ongoing student project: https://github.com/greysondn/fallcorp/tree/fe5f2a317ff3f3206e7dd318cb50f9f67519b02b
The two relevant classes are net.darkglass.Appmanager and net.darkglass.arasql.command.ExecuteCommand.
The issue arises around a combination of ln 51 forwards in AppManager and ln 56 forwards in ExecuteCommand. Loops for Scanner to manage user input and loops for Scanner to manage reading a file line by line aren't compatible; as a consequence, neither I nor my professor can fathom a way to combine the two cases of Scanner into one method.
In other words, these two structures are extremely similar at the end of the day, and should probably be one and the same, but there is no way we could find that wasn't worse than the current state of affairs.
Is there a way to write a Scanner such that it works both for user input and for file input of what a user would have input?
Some quick observations:
I have noted in my code where things start to reek a bit; that is to say, where things feel intuitively wrong. This happens in ExecuteCommand as it was the second of the two to be written.
This code loosely follows the Interpreter design pattern. My native is more Pythonic and/or C++. Some idioms and ways of handling things will undoubtedly reflect this.
My professor is well aware that I at least intended to post this question and is just as curious and agitated as I am. When he solved the project to make sure it was doable and how long it would take, he hit the same stumbling block and couldn't find a solution that he was satisfied with.
EDIT: Context is important; please do pop open those two files at the indicated points and examine them a bit. What ultimately happens as it stands right now is that the two scanners are almost the same, but one only works for files and one only works for user input due to the way the two types of I/O work in Java. On it's face, I suddenly realize the question probably sounds a lot denser than it actually is. (Yes, any method using a Scanner can parse a string regardless of source, but the case here is more about using the same Scanner on two different sources due - mostly - to how it is used).
Edit2: After some commentary, here is some form of code that demonstrates the core problem.
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());
}
}
They both simply scan over an input and do what it says; what would it take to consolidate these two methods into one? (Yes, it's quick and messy; it's at least got the point across and basic commentary.)
回答1:
It seems like you are over-complicating the problem by thinking too much about Scanner
, which is irrelevant to the problem. If you have code that matches to 99%, the straight-forward solution is to move the common code into a method on its own and have two small specialized methods remaining:
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());
}
The specialized methods still contain the loop statement, but there’s nothing wrong with that, as the loops are different.
Still, there is the alternative of moving the difference out of the original code, rather than the common code, e.g.
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());
}
}
There is no advantage of moving the loop statement into the common code in this specific example, but there are scenarios where having the loop maintenance in a framework provides advantages, the Stream
API is just one example…
That said, looking at your specific code, there seems to be a fundamental misconception. In Java, String
object are immutable and calling concat
on a String
creates a new instance. So a variable declaration with an initializer like String test = "";
does not “pre-reserve a space for the thing”, it wastes resources by initializing the variable with a reference to an empty string, which gets overwritten by the subsequent test = cin.nextLine();
anyway. Also, test = test.concat(" "); test = test.concat(cin.nextLine());
needlessly creates intermediate string instances where the much simpler test = test + " " + cin.nextLine();
compiles to code using a builder—for free.
But in the end, these operations are obsolete if you stop ignoring the power of the Scanner
. This class is not just another way of delivering the already existing BufferedReader.readLine()
functionality, it’s a pattern matching tool allowing to use the regex engine on a stream input.
If you want to separate commands by a semicolon, use a semicolon as delimiter instead of reading lines that have to be concatenated manually. Replacing newlines of multi-line commands and removing comments can be done by a single pattern replace operation too. For example
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);
}
}
will print
Command: a single line command
Command: a multi line command
Command: multi -- line command with double minus
Command: and just a last command
If you want to preserve the semicolons in the result, you can change the delimiter from";(\\R|\\Z)"
to "(?<=;)(\\R|\\Z)"
.
回答2:
I think this will solve your problem. This is basically StdIn
class taken from Princeton's Algorithms, 4th edition by Robert Sedgewick and Kevin Wayne. I've used this class in their Coursera MOOC Algorithms Part I to read input either from a file or a cmd.
EDIT
Combine class mentioned above with In class from the same repository.
EDIT 2
In the In
class you have two methods:
/**
* 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);
}
}
and
/**
* 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;
}
You call the proper one based on user input (say you check if the first argument is a file, which is fairly easy to do) and then you have plethora of other methods like hasNextLine
or readChar
. This is all in one class. Doesn't matter which constructor you'll use, you can call the same methods.
回答3:
I would separate out the input from the main code, so you have an interface Source
which you poll for a command, and then have 2 concrete implementations.
FileSource
which just reads from a fileInteractiveSource
one which displays a prompt to the user and returns the text they return
You can pass one of these to your class, and it use it the same regardless of where it is getting it's commands from.
This has the advantage that if you ever wanted to add NetworkSource
or UISource
you would just need to write a new Source
implementation.
This way also lets you close the system cleanly, in that each Source could have a method keepGoing()
, which you could call between commands to see if the system should shutdown, for files it would be when there are no more lines, for input it could be after they type in exit
来源:https://stackoverflow.com/questions/46123566/java-combine-scanner-for-file-with-scanner-for-user-input