The basic problem is that java doesn't do anything at all fancy to parse the string you give it and break it nicely into arguments.
The short answer is to use the String [] version of Runtime.exec:
Runtime.getRuntime().exec(
new String[] {"/bin/bash", "myscript", "arg1", "arg2", "", "arg4"});
If you have other places where the argument parsing is more convoluted than that, you can pass the parsing of the string off to bash, and do:
Runtime.getRuntime().exec(
new String[] {"/bin/bash", "-c", "/bin/bash myscript arg1 arg2 '' arg4"});
If you're converting something over that's doing a huge number of complicated redirects like 2>&1 or setting up a whole pipeline, you might need this bash -c trick.
EDIT:
To understand what's going on here, you have to realize that when user-space code tells the kernel "load this executable with these arguments and start a process based on that" (*), what's passed on to the kernel is an executable, an array of strings for arguments (the 0th/first element of this argument array is the name of the executable, except when doing something weird), and an array of strings for the environment.
What bash does when it sees this line:
/bin/bash myscript arg1 arg2 '' arg4
is think "Okay, /bin/bash isn't a builtin, so I'm executing something. Let's put together the argument array for the subprocess using my string parsing algorithms, which know about quotes and whatnot". Bash then determines that the arguments to pass the kernel for the new process are:
/bin/bash
myscript
arg1
arg2
(empty string)
arg4
Now bash has pretty complicated string processing algorithms that it applies here - it accepts two different kinds of quotes, it'll do expansion of $VAR when it happens outside strings or inside double quotes, it'll replace subcommands in backquotes with the output, etc.
Java doesn't do anything so sophisticated when you call the single string version of exec. It just creates a new StringTokenizer and uses that to break up the string you give it into arguments. That class doesn't know anything about quotes; it splits that string up into:
Java then calls the String[] version of exec. (Well, one of them)
Notes for people picking nits with this:
(*) Yes, I'm deliberately eliding the difference between the system calls fork and execve. Pretend they're one call for the moment.