Java - combine Scanner for file with Scanner for user input

可紊 提交于 2019-12-11 15:48:53

问题


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 file
  • InteractiveSource 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!