Your code is very clearly written and formatted and it's easy to read. You have learned a few anti-patterns that I think you should try to unlearn.
Declaring variables in one line is not something that is usually done in common Java coding styles. It makes it harder to find the declarations.
Always initialising variables to a default special value defeats useful compiler warnings. If you leave them uninitialized, the compiler will warns you when you try to access an accidentally uninitialized field.
Using a special value as to mark "uninitialized" fields robs you from a useful use case: if a user wants to use the application as a part of a script, they cannot just use it to encrypt any string privided by some other program, they have to check if the value is empty first. Java has a universal value for uninitialized fields: null.
The field names are unnecessarily short. A field containing a file name should be named as such. So instead write your field declarations like this:
String inputFileName = null;
String outputFileName = null;
String inputData = null;
When you have a distinct set of input values, use enumerations to represent them in code instead of string constants:
private enum Mode {
ENCRYPT,
DECRYPT
}
And then...
String mode;
Command line parsin is tedious and boring. There are libraries for it, like Apache Commons-CLI. You are also dividing the command line parsing responsibility between the Main and the EncryptDecrypt classes by passing all the parameters parsed from the command line to EncryptDecrypt as such and making that class handle the error checking. You should try to follow the single responsibility principle and make the Main class be fully responsible for the command line and leave just the encryption to EncryptDecrypt. This will require rethinking of what the encryption algorithms need as input.
If you remove the responsibilities that are not strictly related to the encryption, you'll see that the encryption algorith only needs a single character and the encryption key as input.
public interface SubstitutionCipher {
char encrypt(char ch);
}
The next step is figuring out how to pass file and command line data as input to the same cipher without forcing it to handle file IO. As the cipher works on text, one way would be to make the cipher operate on a Reader and convert the command line parameters to either FileReader or StringReader and FileWriter or StringWriter depending on input.
Then, to connect the Main class and the SubstitutionCipher you need a third class that reads a character from the reader, passes it to the cipher and writes the result to the writer.
Catching and logging exceptions in an application like below is not a good idea. The exception you catch here prevents the application from working, so you should end the application with an error message. So let the exception propagate to Main class, catch it, write an error to System.err and exit with an error code. This is the rare case where System.exit(int) should be used.
} catch (IOException e) {
e.printStackTrace();
}