*I'm a java/programming newby.
I've written the following code examples to try to figure out my problem and then give clear examples of my issue. I want to have my program divided into separate classes based on their functions within the application: Main MainController - drives the JavaFX GUI. Initialiser - semantic validation of entered data. Model - performs calculations on the data and sends results back to MainController.
Where I'm stuck is that the initialiser, specifically the linkedhashmap and first validation check, throw a nullpointerexception. This DOESN'T happen if everything is in a single class (I started with everything in one class now am learning how to structure in separate classes, etc.). It also DOESN'T happen if I don't use any JavaFX features.
For example, this works:
package helloWorld;
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello world!");
HashmapExample hashy = new HashmapExample();
System.out.println(hashy.initialiser());
}
}
package helloWorld;
import java.util.LinkedHashMap;
import java.util.Map;
public class HashmapExample {
private Map<String,String> testMap = new LinkedHashMap<>();
public void mapper() {
testMap.put("one","a");
testMap.put("two","b");
testMap.put("three","c");
}
public String initialiser() {
mapper();
String errorField = testMap.entrySet().stream().filter(entry -> entry.getKey().isBlank()).map(Map.Entry::getValue).findFirst().orElse(null);
String message;
if (errorField != null) {
return message = errorField + " blank!";
} else {
return message = "OK!";
}
}
}
But this doesn't:
package helloWorldFX;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
try {
VBox root = (VBox)FXMLLoader.load(getClass().getResource("MainGUI.fxml"));
Scene scene = new Scene(root,400,400);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
package helloWorldFX;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.TextArea;
public class MainController {
@FXML private TextArea outputMessage;
public void run(ActionEvent event) {
Initialiser initialiser = new Initialiser();
outputMessage.setText(initialiser.checker());
}
}
package helloWorldFX;
import java.util.LinkedHashMap;
import java.util.Map;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
public class Initialiser {
@FXML private TextField one;
@FXML private TextField two;
@FXML private TextField three;
private Map<TextField,String> testMap = new LinkedHashMap<>();
public void mapper() {
testMap.put(one,"a");
testMap.put(two,"b");
testMap.put(three,"c");
}
public String checker() {
mapper();
String errorField = testMap.entrySet().stream().filter(entry -> entry.getKey().getText().isBlank()).map(Map.Entry::getValue).findFirst().orElse(null);
String message;
if (errorField != null) {
return message = errorField + " blank!";
} else {
return message = "OK!";
}
}
}
When I do a coverage run in eclipse, I can tell it is probably caused by the line
String errorField = testMap.entrySet().stream().filter(entry -> entry.getKey().isBlank()).map(Map.Entry::getValue).findFirst().orElse(null);
but I'm not completely sure.
I've just noticed in scene builder that the fields 'one', 'two', and 'three' don't appear as valid ID's for the text fields, because they aren't in the MainController class. This must have something to do with the issue, but I don't understand how to fix it.
FXML file, as requested: just remember, its a set of example code, not meant to be a real program, so the layout is nonsensical.
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.VBox?>
<VBox prefHeight="300.0" prefWidth="300.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="helloWorldFX.MainController">
<children>
<TextField />
<TextField />
<TextField />
<Button mnemonicParsing="false" onAction="#run" text="Button" />
<TextArea fx:id="outputMessage" prefHeight="200.0" prefWidth="200.0" />
</children>
</VBox>
Error messages:
Exception in thread "JavaFX Application Thread" java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
at javafx.fxml/javafx.fxml.FXMLLoader$MethodHandler.invoke(FXMLLoader.java:1787)
at javafx.fxml/javafx.fxml.FXMLLoader$ControllerMethodEventHandler.handle(FXMLLoader.java:1670)
at javafx.base/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
at javafx.base/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
at javafx.base/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)
at javafx.base/javafx.event.Event.fireEvent(Event.java:198)
at javafx.graphics/javafx.scene.Node.fireEvent(Node.java:8890)
at javafx.controls/javafx.scene.control.Button.fire(Button.java:203)
at javafx.controls/com.sun.javafx.scene.control.behavior.ButtonBehavior.mouseReleased(ButtonBehavior.java:206)
at javafx.controls/com.sun.javafx.scene.control.inputmap.InputMap.handle(InputMap.java:274)
at javafx.base/com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
at javafx.base/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
at javafx.base/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
at javafx.base/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
at javafx.base/javafx.event.Event.fireEvent(Event.java:198)
at javafx.graphics/javafx.scene.Scene$MouseHandler.process(Scene.java:3862)
at javafx.graphics/javafx.scene.Scene.processMouseEvent(Scene.java:1849)
at javafx.graphics/javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2590)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:409)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:299)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:389)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$2(GlassViewEventHandler.java:447)
at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:411)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:446)
at javafx.graphics/com.sun.glass.ui.View.handleMouseEvent(View.java:556)
at javafx.graphics/com.sun.glass.ui.View.notifyMouse(View.java:942)
at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:174)
at java.base/java.lang.Thread.run(Thread.java:835)
Caused by: java.lang.reflect.InvocationTargetException
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at com.sun.javafx.reflect.Trampoline.invoke(MethodUtil.java:76)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:567)
at javafx.base/com.sun.javafx.reflect.MethodUtil.invoke(MethodUtil.java:273)
at javafx.fxml/com.sun.javafx.fxml.MethodHelper.invoke(MethodHelper.java:83)
at javafx.fxml/javafx.fxml.FXMLLoader$MethodHandler.invoke(FXMLLoader.java:1782)
... 46 more
Caused by: java.lang.NullPointerException
at helloWorldFX.Initialiser.lambda$0(Initialiser.java:29)
at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:176)
at java.base/java.util.Spliterators$IteratorSpliterator.tryAdvance(Spliterators.java:1812)
at java.base/java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:127)
at java.base/java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:502)
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:488)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
at java.base/java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:150)
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
at java.base/java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:543)
at helloWorldFX.Initialiser.checker(Initialiser.java:29)
at helloWorldFX.MainController.run(MainController.java:15)
... 58 more
Work-around:
So based on the comments and some more thought on what could be the cause, I've come up with this work-around.
I instantiate and populate the Map
in the MainController, then instantiate the Initialiser class, which now takes the Map
as a parameter via the constructor. Then I call the checker method from the Initialiser object, which runs the semantic check on the data that was sent to the object when it was instantiated.
As far as I can tell, the issue is that one fxml file can only deal with one controller class, so you can't populate the map with TextFields
from anywhere except the main controller class. If you do, the getText
method will throw nullpointerexception
because it is pointing to TextFields
that don't technically exist in the fxml.
See the amended code:
package helloWorldFX;
import java.util.LinkedHashMap;
import java.util.Map;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
import javafx.scene.control.TextArea;
import javafx.event.ActionEvent;
public class MainController {
@FXML private TextField one;
@FXML private TextField two;
@FXML private TextField three;
@FXML private TextArea outputMessage;
public void run(ActionEvent event) {
Map<TextField, String> testMap = new LinkedHashMap<>();
testMap.put(one,"a");
testMap.put(two,"b");
testMap.put(three,"c");
Initialiser initialiser = new Initialiser(testMap);
outputMessage.setText(initialiser.checker());
}
}
package helloWorldFX;
import java.util.Map;
import javafx.scene.control.TextField;
public class Initialiser {
private Map<TextField, String> values;
public Initialiser(Map<TextField, String> mappedValues) {
this.values = mappedValues;
}
String message;
public String checker() {
String errorField = values.entrySet().stream().filter(entry -> entry.getKey().getText().isBlank()).map(Map.Entry::getValue).findFirst().orElse(null);
if (errorField != null) {
return message = errorField + " blank!";
} else {
return message = "OK!";
}
}
}
At this point, because of my lack of experience, I don't know if this is actually a work-around or the true correct way to do it - maybe someone can help fill that part in.