Showing posts with label Java. Show all posts
Showing posts with label Java. Show all posts

JavaFX Project Setup

Introduction

Using JavaFX platform, we can build applications for desktop, mobile or embessed systems. It has large set of components to design rich user interface.

JavaFX allows,

  • designing the user interface interactively using Scene Builder.
  • testing the user inteface using TestFX.
  • styling the user interface with the help of CSS.

JavaFX historically used to part of Java installation, however, now it is a standalone component which works on top of JDK.

I faced multiple challenges during setting up the JavaFX with Java 21 to build a simple application recently. I would like to share the setup I ended up with which may help you as well.

We will be going to use following tools,

Here we will going to use Maven which will take care of downloading required modules, however, you can use JavaFX SDK as well.

Project Setup

IntelliJ bundles JavaFX plugin in it.

Create New Project in IntelliJ.

Select JavaFX in side bar. Select Project SDK as Java 21.

For now, we do not need any additional dependencies, we will just click on Finish.

This will take time finish setting up project and indexing.

Once it’s ready we can simply run the Main class which is HelloApplication.java in this case.

It should run successfully and show a window with a button “Hello!”.

As you can see some gibberish text is shown. You may see following logs in Console.

This looked like some issue with the fonts. So I had to add following line in main method of HelloApplication.java.

scene.getRoot().setStyle("-fx-font-family: 'serif'");

After running it again it appeared like following,

Changes in POM

If you go to Project Structure > Modules you will find many OpenFX dependencies are added.

If we look into pom.xml file, we can see following dependecies are add for JavaFX.

...
<dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-controls</artifactId>
    <version>11.0.2</version>
</dependency>
<dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-fxml</artifactId>
    <version>11.0.2</version>
</dependency>
...

By default the selected version is 11.0.2 which we will going to change to recent version 22.

<dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-controls</artifactId>
    <version>22</version>
</dependency>
<dependency>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-fxml</artifactId>
    <version>22</version>
</dependency>

However, the build started failing with module error.

I had no clue what could have went wrong, so I did clean up and try to build again but didn’t work.

Surprisingly, when I executed with command line using mvn it worked!

mvn clean compile
mvn javafx:run

Strangely, there is no error in module-info.java, from command line it worked well but failing when I run it from IntelliJ. I felt this issue is something specific to IntelliJ and not to JavaFX.

I found the similar problem is discussed on the forum.

I was using IntelliJ CE version shown in following image,

Moreover, importing the same project to IntelliJ Ultimate Edition worked miraculously without any modifications.

The javafx-maven-plugin is already on latest version, thus we are not changing anything here for now.

...
<plugin>
    <groupId>org.openjfx</groupId>
    <artifactId>javafx-maven-plugin</artifactId>
    <version>0.0.8</version>
    <executions>
        <execution>
            <!-- Default configuration for running with: mvn clean javafx:run -->
            <id>default-cli</id>
            <configuration>
                <mainClass>com.example.hellojavafx/com.example.hellojavafx.HelloApplication</mainClass>
                <launcher>app</launcher>
                <jlinkZipName>app</jlinkZipName>
                <jlinkImageName>app</jlinkImageName>
                <noManPages>true</noManPages>
                <stripDebug>true</stripDebug>
                <noHeaderFiles>true</noHeaderFiles>
            </configuration>
        </execution>
    </executions>
</plugin>
...

Initialisation Error

During build and trying some maven command, I ended up with following error,

Error occurred during initialization of boot layer
java.lang.module.FindException: Module com.example.hellojavafx not found

some articles were suggesting to validate Java and JavaFX versions but that wasn’t the issue. Strangely, deleting target directory and running the application worked as it is.

Also, while running through command for the first time you may get following error,

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project hello-javafx: Fatal error compiling: error: invalid target release: 0 

I noticed that in maven-compiler-plugin the source and target version was 0 which I changed to 21 to fix this issue.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
        <source>0</source>
        <target>0</target>
    </configuration>
</plugin>

Module Info

Apart from pom.xml, you will find one module-info.java shown below. Here we can see module dependency on javafx.controls and javafx.fxml is mentioned. Also, javafx.fxml needs to access controllers we will define in out package thus we are opening it.

module com.example.hellojavafx {
    requires javafx.controls;
    requires javafx.fxml;


    opens com.example.hellojavafx to javafx.fxml;
    exports com.example.hellojavafx;
}

Whenever you introduce new dependency which you want to be available in your package or you want to open your package to external dependency. Make sure to declare it in module-info.java file.

Executable JAR

To make a runnable .jar I used maven-shade-plugin.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>2.3</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>com.example.hellojavafx.HelloApplication</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

I simple ran mvn clean package to create the jar file. However when I ran the jar with following command.

java -jar target/hello-javafx-1.0-SNAPSHOT.jar

It gave me following error,

Error: JavaFX runtime components are missing, and are required to run this application

To solve this, I had to declare an entry point class which can point to my Application class.

package com.example.hellojavafx;

public class EntryPoint {

    public static void main(String[] args) {
        HelloApplication.main(args);
    }
}

This new class I had to use in plugin instead of HelloApplication.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-shade-plugin</artifactId>
    <version>2.3</version>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <goal>shade</goal>
            </goals>
            <configuration>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>com.example.hellojavafx.EntryPoint</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </execution>
    </executions>
</plugin>

And this time it worked! (with warning)

➜  hello-javafx java -jar target/hello-javafx-1.0-SNAPSHOT.jar
Aug 10, 2024 10:35:43 PM com.sun.javafx.application.PlatformImpl startup
WARNING: Unsupported JavaFX configuration: classes were loaded from 'unnamed module @673ba40c'

The shade plugin adds everything to the classpath and since JavaFX relies on modules this setup breaks the execution. Thus, as a workaround we introduce another class which can be an entry point to the execution instead of JavaFX starter application.

Basically, if JavaFX application is loaded through classpath it actually breaks the encapsulation provided by modular approach. Here is the ticket for this warning.

Base64 Encoder

Base64 is popular system that transforms the binary data into limited set of 64 characters.

For example, text abc can be represented in binary as 011000010110001001100011, Base64 divides this binay data into group of 6 bits 011000 010110 001001 100011 and converts it to as per the encoding table.

Base64 Encoding Table has total 64 (2^6) characters.

  • 0 to 25 : A to Z
  • 26 to 51 : a to z
  • 52 to 61 : 0 to 9
  • 62 : Plus symbol ‘+’
  • 63 : Forward slash ‘/’

Additional symbo ‘=’ is used for padding in case of bits are not in multiple of 6.

According to encoding table, 011000 010110 001001 100011 which is 24 (011000) 22 (010110) 9 (001001) 35 (100011) converted to YWJj in Base64 format.

public class Encoder {

    private static final Encoder ENCODER = new Encoder();

    private static final int ENCODE_BASE = 6;

    private final char[] encodingTable = new char[64];

    private Encoder() {
        initEncodingTable();
    }

    public static Encoder getInstance() {
        return ENCODER;
    }

    public String encode(String input) {
        int[] binary = toBinary(input);
        StringBuilder encoded = new StringBuilder();

        for (int start = 0; start < binary.length; start += ENCODE_BASE) {
            int end = Math.min(start + ENCODE_BASE, binary.length - 1);

            int power = ENCODE_BASE - 1;
            int charIndex = 0;
            for (int j = start; j < end; j++) {
                charIndex += (binary[j] * Math.pow(2, power--));
            }

            encoded.append(encodingTable[charIndex]);
        }

        int pad = binary.length % ENCODE_BASE;
        encoded.append("=".repeat(pad));
        return encoded.toString();
    }

    private int[] toBinary(String input) {
        final int bits = 8;
        int[] binary = new int[input.length() * bits];

        try {
            byte[] bytes = input.getBytes(StandardCharsets.UTF_8);
            int index = 0;
            int max = (int) Math.pow(2, (bits - 1));

            for (byte b : bytes) {
                int val = b;
                for (int i = 0; i < bits; i++) {
                    binary[index++] = ((val & max) == 0 ? 0 : 1);
                    val <<= 1;
                }
            }
        } catch (RuntimeException e) {
            e.printStackTrace();
        }
        return binary;
    }

    private void initEncodingTable() {
        int index = 0;

        for (char i = 'A'; i <= 'Z'; i++)
            encodingTable[index++] = i;

        for (char i = 'a'; i <= 'z'; i++)
            encodingTable[index++] = i;

        for (char i = '0'; i <= '9'; i++)
            encodingTable[index++] = i;

        encodingTable[62] = '+';
        encodingTable[63] = '/';
    }
}

This program is implemented to make it more easier to understand. However, for optimal approach we can look into java.util.Base64 class which is bit complex to understand and considers much bigger character set and scenarios than the implemented one.

Some useful stream operations

Separate Odd and Even numbers (List<Integer> to Map<String, List<Integer>>)

Map<String, List<Integer>> oddEvenMap = numbers.stream()  
        .collect(Collectors.groupingBy(i -> (i % 2 != 0 ? "Odd" : "Even")));

Find distinct values from Map<String, List<Integer>

Set<Integer> set = map.values().stream()
					.flatMap(Collection::stream)
					.collect(Collectors.toSet());

Biggest Odd and Even from List (List<Integer> to Map<String, Integer>)

Map<String, Integer> bigOddEven = numbers.stream().collect(Collectors.toMap(i -> (i % 2 == 0 ? "Even" : "Odd"),  Function.identity(), (i1 , i2) -> i1 > i2 ? i1 : i2));

Use value as key and create new map from existing

Map<String, Integer> map = new HashMap<>();  
map.put("a", 1);  
map.put("b", 1);  
map.put("c", 2);  
  
Map<Integer, List<String>> ans = map.keySet().stream().collect(Collectors.groupingBy(map::get));  
System.out.println(ans);

Output

{1=[a, b], 2=[c]}

Find frequency of each number in list

List<Integer> numbers = new ArrayList<>();  
numbers.add(1);  
numbers.add(2);  
numbers.add(3);  
numbers.add(4);  
numbers.add(4);  
  
Map<Integer, Long> ans = numbers.stream()  
        .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));  
System.out.println(ans);

Output

{1=1, 2=1, 3=1, 4=2}

Remove from map based on condition on values

Map<String, Integer> map = new HashMap<>();  
map.put("a", 1);  
map.put("b", 1);  
map.put("c", 2);  
  
map.values().removeIf(i -> i == 1);  
System.out.println(map);

Output

{c=2}

Distinct and Sum of numbers

Stream<Integer> distinct = numbers.stream().distinct();
int s = numbers.stream().mapToInt(Integer::intValue).sum();

IntSummaryStatistics for Average, Sum, Min/Max

IntSummaryStatistics intSummaryStatistics = numbers.stream().mapToInt(Integer::intValue).summaryStatistics();
double average = intSummaryStatistics.getAverage();
long sum = intSummaryStatistics.getSum();
int max = intSummaryStatistics.getMax();
int min = intSummaryStatistics.getMin();
long count = intSummaryStatistics.getCount();

Java 9 to 19

Java 8 is still extensively used in the industry and many applications will gradually shift to newer Java version, especially the LTS versions.

In this post we will take a look at the evolution happened in Java language from Java 9 to Java 19. Note that each version comes with many improvements, bug fixes and variety of features, we will cover the ones which are majorly used and can impact our day to day developement.

Java 9

Factory methods for collection

    List immutableL = List.of(1, 2, 3);
    Map immutableM = Map.of(1, "ONE", 2, "TWO", 3, "THREE")

JShell: Java Shell, or REPL (Read Evaluate Print Loop) to execute java constructs directly in command line.

Private methods in interface.

This will avoid code duplication and better separation of concern when it comes to implementing default and static methods in interface.

interface Student {
    private String joinNames(String firstName, String lastName) {
        return String.join(firstName, " ",lastName);
    }
    private static String schoolName() {
        return "Some School";
    }

    default String id(String firstName, String lastName) {
        String fullName = joinNames(firstName, lastName);
        return schoolName() + "\n" + fullName;
    }
}

Step in direction to optimize String concatenation.

For the given class,

public class Test {
    public static void main(String[] args) {
        String str = args[0] + " and " + args[1];
    }
}

If we compile and check the bytecode, we can notice significant different in the way concatenation is handled.

In Java 8,

➜  java git:(main) ✗ java -version 
openjdk version "1.8.0_362"
OpenJDK Runtime Environment (build 1.8.0_362-bre_2023_01_22_03_30-b00)
OpenJDK 64-Bit Server VM (build 25.362-b00, mixed mode)
➜  java git:(main) ✗ clear           
➜  java git:(main) ✗ java -version
openjdk version "1.8.0_362"
OpenJDK Runtime Environment (build 1.8.0_362-bre_2023_01_22_03_30-b00)
OpenJDK 64-Bit Server VM (build 25.362-b00, mixed mode)
➜  java git:(main) ✗ javac Test.java
➜  java git:(main) ✗ javap -c  Test 
Compiled from "Test.java"
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class java/lang/StringBuilder
       3: dup
       4: invokespecial #3                  // Method java/lang/StringBuilder."<init>":()V
       7: aload_0
       8: iconst_0
       9: aaload
      10: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      13: ldc           #5                  // String  and
      15: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      18: aload_0
      19: iconst_1
      20: aaload
      21: invokevirtual #4                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      24: invokevirtual #6                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      27: astore_1
      28: return
}

In Java 9,

➜  java git:(main) ✗ java -version 
openjdk version "9"
OpenJDK Runtime Environment (build 9+181)
OpenJDK 64-Bit Server VM (build 9+181, mixed mode)
➜  java git:(main) ✗ javac Test.java
➜  java git:(main) ✗ javap -c  Test 
Compiled from "Test.java"
public class Test {
  public Test();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: aload_0
       1: iconst_0
       2: aaload
       3: aload_0
       4: iconst_1
       5: aaload
       6: invokedynamic #2,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
      11: astore_1
      12: return
}

Notice the multiple StringBuilder invocations in case of Java 8, which is replaced with makeConcatWithConstants in Java 9.

Java 10

Local variable type interface, use var to declare.

    var i = 1;
    var str = "Hello";
    var student = getStudent();

Static factory methods to create immutable copy of Collection,

    List<String> immutable = List.copyOf(otherList);

orElseThrow() in Optional

    Optional<Object> optional = Optional.ofNullable(null);
    optional.orElseThrow(() -> new RuntimeException("Something went wrong!"));

Java 11 (LTS)

Execute Java file directly without compiling with javac.

java command internally takes care of the compilation.

Some helper methods for String

String str = "Hello";
boolean isBlank = str.isBlank();
str.lines().forEach(System.out::println);
str = str.strip();
// str.stripLeading();
//  str.stripTrailing();

Removed deprecated packages,

java.xml.ws
java.xml.bind
java.activation
java.xml.ws.annotation
java.corba
java.transaction
java.se.ee
jdk.xml.ws
jdk.xml.bind

Make file read and write convenient,

Path path = Files.writeString(Files.createTempFile("temporary", ".txt"), "Something to write!");

String fileContent = Files.readString(path);
System.out.println(fileContent);

Java 12

Switch can be an expression, changes are in preview.

Before Java 12

Animals animal = Animals.COW;

switch (animal) {
    case COW:
    case GOAT:
        System.out.println("Herbivore");
        break;
    case TIGER:
    case LION:
        System.out.println("Carnivore");
        break;
}

Can now be reduced to,

String animalType = switch (animal) {
    case COW, GOAT -> "Herbivore";
    case TIGER,  LION -> "Carnivore";
};

No need to typecast for instanceof,

if(object instanceof String) {
    System.out.println(((String)object).toUpperCase());
}

but now you can do,

if(object instanceof String str) {
    System.out.println(str.toUpperCase());
}

Compare files,

try {
    Path filePath1 = Files.createTempFile("abc1", ".txt");
    Path filePath2 = Files.createTempFile("abc2", ".txt");

    Files.writeString(filePath1, "Hello!");
    Files.writeString(filePath2, "Hello! (Diff)");

    long mismatchIndex = Files.mismatch(filePath1, filePath2);
    if(mismatchIndex == -1) {
        System.out.println("Both files are same!");
    } else {
        System.out.println("Mismatch found at " + mismatchIndex);
    }
} catch (IOException e) {
    throw new RuntimeException(e);
}

Output

Mismatch found at 6

String Identation,

String str = "Hello";
for(int i = 0 ; i < 5; i++) {
    System.out.print(str.indent(i));
}

Output

Hello
 Hello
  Hello
   Hello
    Hello

Convenient method to transform String,

String numbers = "1:ONE,2:TWO,3:THREE";
Map<Integer, String> map = numbers.transform(input -> {
    Map<Integer, String> output =
        Arrays.stream(input.split(","))
            .collect(Collectors.toMap(i -> Integer.parseInt(i.split(":")[0]),
                i -> i.split(":")[1]));
    return output;
});
System.out.println(map);

Java 13

Text block support in String,

String textBlock = """
    I can write anything,
    without adding \\n in the String.
    """;
System.out.println(textBlock);

New Methods in String for format,

String anything = "Hello %d and %s".formatted(1, "ONE");

Intoduced yield in switch case, this will replace the break for cases where we want to return the number. Difference between yeild and return is that yeild will return the value to switch invocation while return will return the value to the caller of the method.

int answer = switch (number) {
    case 1:
    case 3:
    case 5:
    case 7:
        yield number;
    default:
        yield -1;
};

Java 14

Preview of records, a data class.

record Person(String name, int age){}

Can be used,

Person person = new Person("Human", 999);
System.out.printf("Person %s, age %d\n", person.name(), person.age());

Things to note about record

  • Can not extend, can not be extended by class
  • Can not be abstract
  • Allows static fields and methods
  • Instance fields can be declared during initialization.
  • Declared fields are private and final
record Person(String name, int age){
    // int anything = 0; // Not allowed
    static int anything = 0;

    public String personDetails() {
        return String.format("Person %s, age %d\n", name(), age());
    }
    Person {
        if(name == "Human") {
            throw new RuntimeException("Invalid name");
        }
    }
}

Records can implement interfaces,

interface Human {
    public String personDetails();
}

record Person(String name, int age) implements Human{
    public String personDetails() {
        return String.format("Person %s, age %d\n", name(), age());
    }
}

It can support multiple constructors as well,

record Person(String name, int age){
    public Person() {
        this("Human", 9999);
    }
    public Person (int age) {
        this("Human", age);
    }
}

Allow trailing space in text block,

String textBlock = """
    I can write anything,
    without adding \\n in the String.\s\s\s
    """;
System.out.println(textBlock);

Java 15

Preview of sealed classes or interfacse,

to allow only specific types which can extend or implement respectively.

public abstract sealed class Animal permits Herbivore, Carnivore {
}

final class Herbivore extends Animal{}
sealed class Carnivore extends Animal{}

//class Unknown extends Animal{} // Not allowed to extend

Subclass of a sealed class must have either of the following modifiers,

  • sealed : Will allow to be extended further by permitted classes.
  • non-sealed : Will allow to be extended further by any classes.
  • final : Will not allow to be extended further.
public abstract sealed class Animal permits Herbivore, Carnivore, Omnivore {
}

final class Herbivore extends Animal {}
sealed class Carnivore extends Animal permits  Tiger{}
non-sealed class Omnivore extends Animal {}

final class Tiger extends Carnivore{}

Records can implement the sealed interfaces,

sealed interface Food permits Creature {
    void doSomething();
} 
record Creature(String name) implements Food {
    @Override
    public void doSomething() {
        System.out.println("Anything");
    }
}

Java 16

Pattern matching in instanceof no longer makes variable implicitly final

if(object instanceof String) {
    object = String.format("Result %s", object); // Would give compile time error prior to Java 16.
    System.out.println(object.toUpperCase());
}

New Vector API, incubator.

int[] odd = {1, 3, 5, 7};
int[] even = {2, 4, 6, 8};
var vector1 = IntVector.fromArray(IntVector.SPECIES_128, odd, 0);
var vector2 = IntVector.fromArray(IntVector.SPECIES_128, even, 0);
var vector3 = vector1.add(vector2);
System.out.println(vector3);

Output

[3, 7, 11, 15]

Note that, to run the program you will need to add the module otherwise it won’t be visible.

 java --add-modules jdk.incubator.vector JavaMainClass

Java 17 (LTS)

null in switch,

switch (number) {
    case 1, 2, 3 -> System.out.println("Valid");
    case null -> System.out.println("Not available");
    default -> System.out.println("Invalid");
}

Pattern matching in switch,

String value = switch (obj) {
    case Integer i -> "Integer";
    case Long l    -> "Long";
    case Double d  -> "Double";
    case String s  -> "String";
    case null -> "NULL";
    default -> obj.toString();
};

Java 18

Introduce @snippet in JavaDoc to write code in comments,

/**
* {@snippet:
*  int a = 10;
* }
*/
public void testMethod() {

}

Finalization is deprecated.

The use of finalize() method is discouraged and the support will be removed in future.

Java 19

Preview Virtual Threads

lightweight threads which effectively shares the platform thread for optimal hardware utilisation.

ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();

Structured Concurrency (Incubator)

Sounds interesting, it enables to consider multiple threads as a unit. This will going to give better control over multithreaded programs.