Search code examples
javajavafxnullpointerexceptionlinkedhashmapjava-12

Why can't I generate a map using JavaFX textfields by calling the method from another Class?


*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

Solution

  • 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.