I'm building a UI for a Simulator running in background. Since this Simulator may not hold for a long time, it of course has to be in a separate thread from the JavaFx Thread. I want to start, pause, resume and stop/terminate the Simulator when the corresponding button is clicked.
The service class that advances the simulator looks like this:
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class SimulatorService extends Service<Void> {
private Simulator simulator;
private long cycleLengthMS = 1000;
private final AtomicBoolean simulatorStopped = new AtomicBoolean(false);
public SimulatorService(Simulator simulator){
this.simulator = simulator;
}
#Override
protected Task<Void> createTask() {
return new Task<>() {
#Override
protected Void call() {
System.out.println("Requested start of Simulator");
int state;
do {
// advance
state = simulator.nextStep();
try {
TimeUnit.MILLISECONDS.sleep(cycleLengthMS);
if(simulatorStopped.get()){
super.cancel();
}
} catch (InterruptedException e) {
return null;
}
}
while (state == 0 && !simulatorStopped.get());
return null;
}
};
}
#Override
public void start(){
if(getState().equals(State.READY)){
simulatorStopped.set(false);
super.start();
}
else if(getState().equals(State.CANCELLED)){
simulatorStopped.set(false);
super.restart();
}
}
#Override
public boolean cancel(){
if(simulatorStopped.get()){
simulatorStopped.set(true);
return false;
} else{
simulatorStopped.set(true);
return true; //if value changed
}
}
}
The Simulator starts the Service if a button on the GUI is pressed:
import javafx.application.Platform;
import java.util.concurrent.atomic.AtomicInteger;
public class Simulator {
Model model;
private final SimulatorService simulatorService;
public Simulator(Model model){
this.model = model;
simulatorService = new SimulatorService(this);
}
public int nextStep(){
final AtomicInteger res = new AtomicInteger(0);
Platform.runLater(new Thread(()-> {
res.set(model.nextStep());
}));
return res.get();
}
public boolean stopSimulationService() throws IllegalStateException{
return simulatorService.cancel();
}
public void startSimulationService() throws IllegalStateException{
simulatorService.start();
}
}
Parts of the window are redrawn if a observed value in the model changes:
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class Model {
private final IntegerProperty observedValue = new SimpleIntegerProperty(0);
public int nextStep() {
observedValue.set(observedValue.get() + 1);
return observedValue.get() > 500000 ? 1 : 0;
}
public int getObservedValue() {
return observedValue.get();
}
public IntegerProperty observedValueProperty() {
return observedValue;
}
public void setObservedValue(int observedValue) {
this.observedValue.set(observedValue);
}
}
The redraw happens in another class:
import javafx.beans.value.ChangeListener;
public class ViewController {
private View view;
private Simulator simulator;
private Model model;
public ViewController(Simulator simulator) {
this.simulator = simulator;
this.view = new View();
setModel(simulator.model);
view.nextStep.setOnMouseClicked(click -> {
simulator.nextStep();
});
view.startSim.setOnMouseClicked(click -> {
simulator.startSimulationService();
});
view.stopSim.setOnMouseClicked(click ->{
simulator.stopSimulationService();
});
}
public View getView() {
return view;
}
private final ChangeListener<Number> observedValueListener = (observableValue, oldInt, newInt) -> {
view.updateView(newInt.intValue());
};
public void setModel(Model m) {
this.model = m;
m.observedValueProperty().addListener(observedValueListener);
}
}
The corresponding view:
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Text;
public class View extends BorderPane {
Button nextStep = new Button("next step");
Button startSim = new Button("start");
Button stopSim = new Button("stop");
GridPane buttons = new GridPane();
Text num = new Text();
public View(){
buttons.add(nextStep,0,0);
buttons.add(startSim,0,1);
buttons.add(stopSim,0,2);
buttons.setAlignment(Pos.BOTTOM_LEFT);
setCenter(buttons);
setTop(num);
}
public void updateView(int num){
this.num.setText("" + num);
}
}
Main:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
ViewController c = new ViewController(new Simulator(new Model()));
Scene scene = new Scene(c.getView(),200,200);
stage.setScene(scene);
stage.show();
}
}
First, I'm sorry for my bad English. I'm a newbie J2ME
I wrote the below code after i watched some tutorial but it not working. I don't know what happen with it.
AnimationCanvas.java
package example;
import java.io.IOException;
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.GameCanvas;
import javax.microedition.lcdui.game.LayerManager;
import javax.microedition.lcdui.game.Sprite;
public class AnimationCanvas extends GameCanvas implements Runnable {
private Sprite ca;
private int frameWidth = 14;
private int frameHeight = 14;
private boolean playing = true;
private int a=1;
public AnimationCanvas(){
super(true);
try {
ca = new Sprite(Image.createImage("ca.png"), frameWidth, frameHeight);
} catch (IOException ex) {
ex.printStackTrace();
}
}
public void start() {
Thread runner = new Thread(this);
runner.start();
}
public void run(){
while (playing){
drawDisplay(getGraphics());
try{
Thread.sleep(10);
} catch(Exception e){}
}
}
public void drawDisplay(Graphics g){
g.setColor(0x000000);
g.fillRect(0,0,getWidth(),getHeight());
ca.setFrame(1);
ca.paint(g);
flushGraphics();
}
public void stop(){
playing=false;
}
}
and AnimationMidlet.java
package example;
import java.io.IOException;
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class AnimationMidlet extends MIDlet {
private Display display;
private AnimationCanvas canvas;
public AnimationMidlet() throws IOException {
display = Display.getDisplay(this);
canvas = new AnimationCanvas();
}
private void initialize() {
// write pre-initialize user code here
// write post-initialize user code here
}
public void startMIDlet() {
// write pre-action user code here
// write post-action user code here
}
public void resumeMIDlet() {
// write pre-action user code here
// write post-action user code here
}
public void switchDisplayable(Alert alert, Displayable nextDisplayable) {
// write pre-switch user code here
Display display = getDisplay();
if (alert == null) {
display.setCurrent(nextDisplayable);
} else {
display.setCurrent(alert, nextDisplayable);
}
// write post-switch user code here
}
public Display getDisplay() {
return Display.getDisplay(this);
}
public void startApp() {
if(canvas!=null){
display.setCurrent(canvas);
canvas.start();
}
}
public void pauseApp() {
}
public void destroyApp(boolean unconditional) {
canvas.stop();
}
}
and the output
Running in the identified_third_party security domain
java.io.IOException
Uncaught exception java/lang/NullPointerException.
at javax.microedition.lcdui.ImmutableImage.getImageFromStream(Image.java:968)
at javax.microedition.lcdui.ImmutableImage.(Image.java:939)
at javax.microedition.lcdui.Image.createImage(Image.java:313)
at example.AnimationCanvas.(AnimationCanvas.java:25)
at example.AnimationMidlet.(AnimationMidlet.java:25)
at java.lang.Class.runCustomCode(+0)
at com.sun.midp.midlet.MIDletState.createMIDlet(+34)
Thanks for your reading!
I am trying to update markers on Google map using a service, but when I use the "setParkingSpotMarker" function that add a marker to every item at the list I get an error "java.lang.IllegalStateException: Not on the main thread"
The function "setParkingSpotMarker" is called in the loop inside the service -DataUpdateService.
I don't know how to change my code so it will update the markers on the main thread.I have read some posts about that but didn't understand how to change it in my code.
this is the service:
package com.example.sailon;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import java.util.ArrayList;
import java.util.Timer;
import java.util.TimerTask;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
public class DataUpdateService extends Service {
Timer myTimer = new Timer();
MyTimerTask myTask = new MyTimerTask();
MyMap mymap;
String teamID;
static LatLng teamLocation;
ArrayList<TeamsList> teamsList= new ArrayList<TeamsList>() ;
public ArrayList<UnitInHeatForMap> unitswithteam= new ArrayList<UnitInHeatForMap>();
String action;
Context fromContext;
String CompName;
String heatNum;
boolean isSailor;
String usermail;
GPSTracker gps;
private final IBinder mBinder = new LocalBinder();
#Override
public void onCreate() {
super.onCreate();
}
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.d("PS", "DataUpdateService onStartCommand");
return Service.START_NOT_STICKY;
}
#Override
// return thee instance of the local binder to the activity
public IBinder onBind(Intent intent) {
Log.d("PS", "DataUpdateService onBind");
return mBinder;
}
public class LocalBinder extends Binder {
DataUpdateService getService() {
Log.d("PS", "DataUpdateService LocalBinder onBind");
return DataUpdateService.this;
}
}
public class MyTimerTask extends TimerTask {
#Override
public void run() {
String unitToUpdate= mymap.getUnitToUpdate (CompName,heatNum,usermail);
Log.d("NY","CompName " +CompName);
Log.d("NY","heatNum " +heatNum);
Log.d("NY","usermail " +usermail);
Log.d("NY","unitToUpdate" +unitToUpdate);
Log.d("NY","isSailor" +isSailor);
if (isSailor){
gps = new GPSTracker(DataUpdateService.this,unitToUpdate );
if( ! (gps.canGetLocation())){
gps.showSettingsAlert();
}
}
unitswithteam=mymap.refresh (CompName,heatNum);
int j= unitswithteam.size();
for (int i=0; i<j;i++){
teamID=unitswithteam.get(i).getTeamID();
Log.d("NY","teamID "+i+teamID);
UnitsInHeats us=unitswithteam.get(i).getUnit();
teamLocation = new LatLng(us.getLat(),us.getLng() );
Log.d("NY","team location "+i + " "+us.getLat());
mymap.setParkingSpotMarker(teamLocation,teamID);
if (i==(j-1)){
CameraPosition secound =new CameraPosition.Builder()
.target(teamLocation)
.zoom(15.5f)
.bearing(300)
.tilt(50)
.build();
mymap.moveMyMapCamera(secound);
}
}
}
}
// called from the activity
public void MapUpdateFromService(Context context, MyMap map, final String action,
String CompName, String heatNum, boolean isSailor , String usermail) {
Log.d("PS", "DataUpdateService MapUpdateFromService");
this.mymap = map;
this.action = action;
this.CompName=CompName;
this.heatNum=heatNum;
this.isSailor=isSailor;
this.usermail=usermail;
// this command activate the run function from the inner class MyTimerTask every 5 seconds.
myTimer.schedule(myTask,0,5000);
}
public void onDestroy() {
super.onDestroy();
// cancel the scheduler.
myTimer.cancel();
}
}
this is the activity that calls the service:
package com.example.sailon;
import java.util.ArrayList;
import com.google.android.gms.maps.MapFragment;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import android.app.ActionBar;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
public class Map extends Activity {
Bundle extras;
// private GoogleMap map;
static LatLng teamLocation;
//public static ArrayList<Teams> teams;
ArrayList<TeamsList> teamsList= new ArrayList<TeamsList>() ;
Marker mark;
double lat;
double lng;
GPSTracker gps;
String Location ;
String teamID;
String CompName;
MyMap mymap;
String heatNum;
String CompID;
boolean isSailor;
String usermail;
static LatLng BeerSheva = new LatLng(31.250919, 34.783916);
public ArrayList<UnitInHeatForMap> unitswithteam= new ArrayList<UnitInHeatForMap>();
// ArrayList<LatLng> List= new ArrayList<LatLng>() ;
DataUpdateService dbuService;
boolean dbuBound=false;// when service connected get true
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_map);
mymap= new MyMap(this,((MapFragment)getFragmentManager().findFragmentById(R.id.map)).getMap());
//map=((MapFragment)getFragmentManager().findFragmentById(R.id.map)).getMap();
extras = getIntent().getExtras();
CompName=extras.getString("CompName");
heatNum=extras.getString("heatNum");
isSailor=extras.getBoolean("isSailor");
usermail=extras.getString("usermail");
Log.d("CompName",CompName);
Log.d("heatNum",heatNum);
// get action bar
ActionBar actionBar = getActionBar();
// Enabling Up / Back navigation
actionBar.setDisplayHomeAsUpEnabled(true);
if (mymap!=null){
Log.d("PS", "map isnt null");
mymap.setMapType();
mymap.setMyLocationEnabled(true);
CameraPosition firstZom =new CameraPosition.Builder()
.target(BeerSheva)
.zoom(15.5f)
.bearing(300)
.tilt(50)
.build();
mymap.moveMyMapCamera(firstZom);
}
}
// serviceConnerction is an interface that must be implemented when using bound service
private ServiceConnection sConnection=new ServiceConnection() {
#Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d("PS", "Map onServiceConnected");
DataUpdateService.LocalBinder binder=(DataUpdateService.LocalBinder)service;
dbuService=binder.getService();
Log.d("PS", "Map callupdate");
dbuService.MapUpdateFromService(Map.this,mymap,"ActionSearch",CompName,heatNum, isSailor,usermail);
dbuBound=true;
}
#Override
public void onServiceDisconnected(ComponentName name) {
Log.d("PS", "Map onServiceDisconnected");
dbuBound=false;
}
};
#Override
protected void onStart() {
super.onStart();
// Bind to DataUpdateService
Log.d("PS", "Map onStart");
Intent intent= new Intent(this,DataUpdateService.class);
bindService(intent,sConnection, Context.BIND_AUTO_CREATE);
}
#Override
protected void onStop() {
super.onStop();
// Unbind from the service
Log.d("PS", "Map onStop");
if(dbuBound){
unbindService(sConnection);
dbuBound=false;
}
}
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.activity_main_actions, menu);
return super.onCreateOptionsMenu(menu);
}
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// Take appropriate action for each action item click
switch (item.getItemId()) {
case R.id.logoutAction:
Intent i = new Intent(Map.this, MainActivity.class);
startActivity(i);
return true;
case R.id.VideoAction:
Intent intent = new Intent("android.media.action.VIDEO_CAMERA");
startActivity(intent);
return true;
case R.id.CallAction:
Intent call = new Intent(Intent.ACTION_DIAL);
startActivity(call);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}
this is the calss of the map:
package com.example.sailon;
import android.content.Context;
import android.util.Log;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MarkerOptions;
import com.parse.ParseException;
import java.util.ArrayList;
/**
* Created by Evyatar.m on 21/02/2015.
*/
public class MyMap {
private GoogleMap map;
static LatLng BeerSheva = new LatLng(31.250919, 34.783916);
String CompID;
String teamID;
String CompName;
String heatNum;
ArrayList<TeamsList> teamsList= new ArrayList<TeamsList>() ;
public ArrayList<UnitInHeatForMap> unitswithteam2= new ArrayList<UnitInHeatForMap>();
static LatLng teamLocation;
Model DB;
public MyMap(Context context, GoogleMap map) {
Log.d("PS", "MyMap builder");
DB = Model.getInstance(context);
this.map = map;
}
public String getUnitToUpdate (String CompName,String heatNum, String usermail){
String unit=null;
try {
CompID= DB.getCompIDByName(CompName);
} catch (com.parse.ParseException e) {
e.printStackTrace();
}
try {
unit= DB.getUnitToUpdateFromDB (CompID,heatNum, usermail);
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return unit;
}
public void setMyLocationEnabled(boolean b){
Log.d("PS", "MyMap set location enable");
map.setMyLocationEnabled(true);
}
public void setParkingSpotMarker(LatLng teamLocation,String teamID) {
Log.d("PS", "ParkingMap setParkinSpotMarker");
map.addMarker(new MarkerOptions()
.position(teamLocation)
.icon(BitmapDescriptorFactory.fromResource(R.drawable.iconsmall))
.title("Team " +teamID)).showInfoWindow();
}
public void setMapType(){
Log.d("PS", "MyMap setType");
map.setMapType(GoogleMap.MAP_TYPE_NORMAL);
}
public void moveMyMapCamera(CameraPosition firstZom) {
Log.d("PS", "MyMap moveParkingMapCamera");
map.moveCamera(CameraUpdateFactory.newCameraPosition(firstZom));
}
//updates all points on map
public ArrayList<UnitInHeatForMap> refresh (String CompName,String heatNum){
Log.d("PS", "MyMap refresh");
try {
CompID= DB.getCompIDByName(CompName);
} catch (com.parse.ParseException e) {
e.printStackTrace();
}
try {
DB.setUnitInHeatForMap (new Model.CallbackModel () {
#Override
public void done (ArrayList<UnitInHeatForMap> unitswithteam){
if (unitswithteam.size() >0){
unitswithteam2=unitswithteam;
}
}
}, CompID, heatNum,this);
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return unitswithteam2;
}
}
please help me
thanks !!!**
The run method of the TimerTask runs on background thread, and you can only update the map on the main thread.
So you have to wrap the following Map UI update codes into runOnUiThread()
mymap.setParkingSpotMarker(teamLocation,teamID);
if (i==(j-1)){
CameraPosition secound =new CameraPosition.Builder()
.target(teamLocation)
.zoom(15.5f)
.bearing(300)
.tilt(50)
.build();
mymap.moveMyMapCamera(secound);
}
You need to have an Activity to apply runOnUIThread, so you need to put your
fromContext = ((Map)context);
inside the first line of your MapUpdateFromService method. Then you can call runOnUiThread() in your TimerTask's run() method.
((Map)fromContext).runOnUiThread(new Runnable() {
#Override
public void run() {
//run your Map UI update code
}
});
This is also a similar issue of this problem.
What is the correct way to manipulate an Observable collection in a thread, where the collection is already bound to a JavaFX UI-node?
In my sample application, the connection between the collection and the nodes are broken before the thread can do any manipulation; and then they are re-connected after the thread is done. The methods are disconnectObservable() and connectObservable() respectively. Without these two methods, java.lang.IllegalStateException: Not on FX application thread is reported.
Ideally I would like ChangeObservableTask to make its changes to mWords, and then I would call some method to tell mObservable to refresh itself and notify its listeners. Is there such a thing?
Thanks.
package theapp;
import java.util.LinkedList;
import java.util.List;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ThreadObList extends Application {
private final List<String> mWords;
private final ObservableList<String> mObservable;
private ListView mListView;
private Label mCount;
public ThreadObList() {
mWords = new LinkedList<>();
mObservable = FXCollections.observableList(mWords);
mWords.add("park");
}
#Override
public void start(Stage primaryStage) {
Button btn = new Button();
btn.setText("Start thread");
btn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
ChangeObservableTask task = new ChangeObservableTask();
Thread thd = new Thread(task);
disconnectObservable();
thd.start();
try {
task.get();
System.out.println("ChangeObservableTask exited normally.");
}
catch(Exception ex) {
System.out.println(ex.getMessage());
}
connectObservable();
}
});
mCount = new Label();
mListView = new ListView();
VBox root = new VBox(5, btn, mCount, mListView);
VBox.setVgrow(mListView, Priority.ALWAYS);
connectObservable();
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
private void connectObservable() {
mListView.setItems(mObservable);
mCount.textProperty().bind(Bindings.size(mObservable).asString());
}
private void disconnectObservable() {
mListView.setItems(null);
mCount.textProperty().unbind();
}
private class ChangeObservableTask extends Task<Void> {
#Override
protected Void call() throws Exception {
mObservable.add("dart");
mObservable.add("truck");
mObservable.add("ocean");
return null;
}
}
}
Once the list is used as the contents of the ListView, you can only manipulate it from the FX Application Thread. See the Task javadocs for a bunch of usage examples.
You can create a copy of your ObservableList and pass it to your task, manipulate the copy and return the results. Then update the ObservableList with the results in the onSucceeded handler.
Also note that you shouldn't make any blocking calls, such as task.get() on the FX Application Thread, as you can make the UI unresponsive by doing so.
So you should do something along the lines of:
btn.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
ChangeObservableTask task = new ChangeObservableTask(new ArrayList<>(mObservable));
Thread thd = new Thread(task);
task.setOnSucceeded(new EventHandler<WorkerStateEvent>() {
#Override
public void handle(WorkerStateEvent event) {
mObservable.setAll(task.getValue());
}
});
thd.start();
}
});
and
private class ChangeObservableTask extends Task<List<String>> {
private final List<String> data ;
ChangeObservableTask(List<String> data) {
this.data = data ;
}
#Override
protected List<String> call() throws Exception {
data.add("dart");
data.add("truck");
data.add("ocean");
return data;
}
}
I am testing an application.
My test is complex, and I spawn 2 thread that start 2 process builders which spawn 2 java processes.
Is it possible to write a custom redirect that will be similar to inherit but prepend something to every out and err message, so that I would know its origin.
Example code below:
public class test {
public static void main(String... args){
Thread t = new Thread(new testHelper());
t.start();
t = new Thread(new testHelper());
t.start();
}
}
import java.io.IOException;
public class testHelper implements Runnable {
#Override
public void run() {
Class klass = testWorker.class;
System.out.println(klass.getCanonicalName());
String separator = System.getProperty("file.separator");
String classpath = System.getProperty("java.class.path");
String path = System.getProperty("java.home")
+ separator + "bin" + separator + "java";
ProcessBuilder processBuilder =
new ProcessBuilder(path, "-cp",
classpath,
klass.getCanonicalName());
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
Process process = null;
try {
process = processBuilder.start();
} catch (IOException e) {
e.printStackTrace();
}
try {
process.waitFor();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Child Process is done");
}
}
public class testWorker {
public static void main(String ... args) throws InterruptedException {
System.out.println("Doing some stuff");
Thread.sleep(10000);
System.out.println("Finished doing some stuff");
}
}
No, its not possible. In the source code for java.lang.ProcessBuilder.Redirect the constructor is private and has this to say
/**
* No public constructors. Clients must use predefined
* static {#code Redirect} instances or factory methods.
*/
private Redirect() {}