Showing posts with label java. Show all posts
Showing posts with label java. Show all posts

Monday, April 17, 2023

String.replace and String.replaceAll are totally different beasts - and named quite badly

We start out with some innocent code that will replace a variable with a certain value:

  public static void main(String[] args) {

  String quote = "$villain$ has an eye.";

        String villain = "Sauron";

  System.out.println(quote.replace("$villain$", villain));

  }

We get this output:

Sauron has an eye.

All is well until we remember that there was also someone called Saurons' Mouth, so we change our quote to:

        String quote = "$villain$ has an eye and $villain$ has a mouth.";

And since we now have multiple occurences of $villain$ we'll also use replaceAll instead of replace, right? Like this:

    public static void main(String[] args) {

        String quote = "$villain$ has an eye and $villain$ has a mouth.";

        String villain = "Sauron";

        System.out.println(quote.replaceAll("$villain$", villain));

    }

Surprisingly, we get this output:

$villain$ has an eye and $villain$ has a mouth.

This is because replaceAll expects a regularExpression as its first parameter and the dollar sign has special significance in regular expressions (denoting "end-of-line"). Even worse, though the presence of two methods named replace / replaceAll suggests that the former will only replace the first occurrence this is not the case, this is not the case. So the correct solution is to indeed still use replace:

    public static void main(String[] args) {
        String quote = "$villain$ has an eye and $villain$ has a mouth.";
        String villain = "Sauron";
        System.out.println(quote.replace("$villain$", villain));
    }

Takeaways:
  • This is a case of bad API design in Javas' String class. 
  • Always check, if parameters to String methods are plain strings or interpreted as regular expressions. 
    • There is also a possible performance penalty when using regular expressions

Wednesday, November 19, 2014

Metaprogramming Trap with Interfaces in Java 8

Consider the following little example:
package prv.rli.codetest;

import java.lang.reflect.Method;

public class BreakingInterfaces  {
    interface Base {
        BaseFoo foo();
        interface BaseFoo {           
        }
    }
   
    interface Derived extends Base {
        DerivedFoo foo();
        interface DerivedFoo extends BaseFoo {
           
        }
    }
   
    public static void main(String[] args) {       
        dumpDeclaredMethods(Derived.class);
    }

    private static void dumpDeclaredMethods(Class<?> class1) {
        System.out.println("---" + class1.getSimpleName() + "---");
        Method[] methods = class1.getDeclaredMethods();
        for (Method method : methods) {
            System.out.println(method);
        }
        System.out.println("----------");
    }
}
What would you expect to be the output? We have a derived Interface which overrides the only method of its base interface in a covariant way. So one method only.

If you compile the above examplt with jdk1.7.0.55 the output is indeed:
 ---Derived---
public abstract BreakingInterfaces$Derived$DerivedFoo BreakingInterfaces$Derived.foo()
----------
But when using jdk1.8.0.25 the output is:
---Derived---
public abstract prv.rli.codetest.BreakingInterfaces$Derived$DerivedFoo prv.rli.codetest.BreakingInterfaces$Derived.foo()
public default prv.rli.codetest.BreakingInterfaces$Base$BaseFoo prv.rli.codetest.BreakingInterfaces$Derived.foo()
----------
 For reasons unknown to me, the base class method resurfaces here which may lead to unwanted behaviour when using reflection.

Thursday, October 16, 2014

Per-row mouse cursor in JavaFX TableViews

Today I want to show how to set the mouse cursor in a TableView on a per-row basis.

In a real-world scenario this can be used to indicate what will happen when you click on the row (will it be selected? will it work like a link? is it unselectable?, etc.). To that end it will often be necessary to access the data displayed in the row.

In our example we will set a crosshair cursor for Persons with the letter 'U' in their first name.

First we setup a row factory for the table:

    private void setupPerRowCursor(TableView<Person> table) {
        table.setRowFactory(this::createTableRow);
    }


That row factory will create the row and listen for changes to its indexProperty:

    private TableRow<Person> createTableRow(TableView<Person> table) {
        TableRow<Person> row =  new TableRow<Person>();
        row.indexProperty().addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
            rowChanged(row, newValue.intValue());
        });
        return row;
    }


Remember that rows (just like cells) will be reused in the table view so even if you have millions of data rows, there will only be a couple of TableRow instances. Therefore we must change react to a row being reused:

    private void rowChanged(TableRow<Person> row, int idx) {
        items = row.getTableView().getItems();
        if (idx >= 0 && idx < items.size()) {
            Person p = items.get(idx);
            if (p.getFirstName().contains("U")) {
                row.setCursor(Cursor.CROSSHAIR);
            }
            else {
                row.setCursor(Cursor.DEFAULT);
            }
        }
    }


We get the (newly associated) data by examining the items of the table. The new index the row points to can be out of bounds, e.g. for TableRows that are displayed but do not have a data row associated, therefore we must check idx to be within limits.

Here's a screenshot with the mouse cursor over a person with the (strange) first name HBZZU:


If you go down with the mouse a bit, the cursor will turn into a normal arrow again.

Here's full source code of the example:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Cursor;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

import org.apache.commons.lang.RandomStringUtils;

public class TableViewMouseCursorPerRow extends Application {

    ObservableList<Person> items = FXCollections.observableArrayList();
    TableView<Person> table = new TableView<Person>();

    public TableViewMouseCursorPerRow() {
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        table.setItems(items);
        TableColumn<Person, String> firstNameCol = new TableColumn<Person, String>("First Name");
        firstNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
        TableColumn<Person, String> lastNameCol = new TableColumn<Person, String>("Last Name");
        lastNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));
        table.getColumns().setAll(firstNameCol, lastNameCol);
        setupPerRowCursor(table);
        addPersons();
        grid.add(table, 0, 1, 3, 1);
       
        Scene scene = new Scene(grid, 400, 400);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void setupPerRowCursor(TableView<Person> table) {
        table.setRowFactory(this::createTableRow);
    }
   
    private TableRow<Person> createTableRow(TableView<Person> table) {
        TableRow<Person> row =  new TableRow<Person>();
        row.indexProperty().addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> {
            rowChanged(row, newValue.intValue());
        });
        return row;
    }

    private void rowChanged(TableRow<Person> row, int idx) {
        items = row.getTableView().getItems();
        if (idx >= 0 && idx < items.size()) {
            Person p = items.get(idx);
            if (p.getFirstName().contains("U")) {
                row.setCursor(Cursor.CROSSHAIR);
            }
            else {
                row.setCursor(Cursor.DEFAULT);
            }
        }
    }

    private void addPersons() {
        for (int i = 0; i < 50; i++) {
            Person p = new Person();
            p.setFirstName(items.size() + " " + RandomStringUtils.random(5, "ABEGHILOUZ"));
            p.setLastName(RandomStringUtils.random(10, "QWRTOUPL"));
            items.add(p);
        }
    }

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

    public class Person {
        private StringProperty firstName;

        public void setFirstName(String value) {
            firstNameProperty().set(value);
        }

        public String getFirstName() {
            return firstNameProperty().get();
        }

        public StringProperty firstNameProperty() {
            if (firstName == null)
                firstName = new SimpleStringProperty(this, "firstName");
            return firstName;
        }

        private StringProperty lastName;

        public void setLastName(String value) {
            lastNameProperty().set(value);
        }

        public String getLastName() {
            return lastNameProperty().get();
        }

        public StringProperty lastNameProperty() {
            if (lastName == null)
                lastName = new SimpleStringProperty(this, "lastName");
            return lastName;
        }
    }
}

Tuesday, October 7, 2014

Setting the preferred width of a ListView to fit its items in JavaFX

Let's assume, we want to create a popup listview, that is toggled by a click on a button. Like this:

import java.util.Random;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.layout.GridPane;
import javafx.stage.Popup;
import javafx.stage.Stage;

import org.apache.commons.lang.RandomStringUtils;

public class ListViewWidth extends Application {
   
    Popup popup = new Popup();   
    ListView<String> listView = new ListView<>();
    Button button = new Button("Popup");
    Random rnd = new Random();

    public ListViewWidth() {
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        grid.add(button, 0, 0);
        button.setOnAction(this::toggle);
        popup.getContent().add(listView);
        Scene scene = new Scene(grid, 640, 480);
        primaryStage.setScene(scene);       
        primaryStage.show();
    }
   
    private void toggle(ActionEvent e) {
        if (popup.isShowing()) {
            popup.hide();
        }
        else {
            ObservableList<String> items = createItems();
            listView.setItems(items);
            Point2D pos = button.localToScreen(0, 0);
            popup.show(button, pos.getX(), pos.getY() + button.getHeight());
        }       
    }
   
    private ObservableList<String> createItems() {
        ObservableList<String> result = FXCollections.observableArrayList();
        for (int i = 0; i < 14; i++) {
            result.add(RandomStringUtils.random(rnd.nextInt(100) + 1, true, true));
        }
        return result;
    }

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


Whenever the button is clicked, we hide the popup, if it is already displayed. If it is not yet displayed we create new strings of random length to act as items for the listview and then we show the popup right underneath the button. The screenshot below shows how the popup will look like.


As you can see, we now have an ugly horizontal scrollbar. To get rid of it, we need to set the preferred width of the list view to the width of the widest element in the list, which is a bit tricky (JavaFX own implementation of the ComboBox is doing it with the aid of a few package private methods which we cannot use here).

The trick is to install your own cell factory
         listView.setCellFactory(this::cellFactory);
and make the cells report the sizes to the listview
    private ListCell<String> cellFactory(ListView<String> view) {
        return new ListCell<String>() {
            @Override
            protected void updateItem(String item, boolean empty) {
                super.updateItem(item, empty);
                setText(item);
                widthProperty().addListener(this::widthChanged);
            }

        };
    }


Whenever a cell reports a changed width, we compare it to the current preferred width of the list view and set the preferred width of the list view to the maximum of both values
private void widthChanged(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
    double width = getWidth() + listView.getInsets().getLeft() + listView.getInsets().getRight();
    listView.setPrefWidth(Math.max(listView.getPrefWidth(), width));
}          
 

Note that you have to add the insets of the listview to the required width in order to get the correct size. Then we have a perfect popup listview beneath our button:


This looks good -- at first sight. Problems arise, if the number of list view entries grow to the point that they are no longer displayable together.
In that case, cells will not be created for the invisible rows and thus the maximum size of all the rows is no longer correct. If you scroll down, the listview will suddenly grow wider, if a cell is encountered whose width is bigger than the current maximum.

To make the problem more pronounced, I've changed createItems() to create more items and increase the length of the items that are further down:
    private ObservableList<String> createItems() {
        ObservableList<String> result = FXCollections.observableArrayList();
        for (int i = 0; i < 140; i++) {
            result.add(RandomStringUtils.random(rnd.nextInt(i+5) + 1, true, true));
        }
        return result;
    }



What we really want is to calculate the width of all the rows in the list. This is exactly what the Method prefWidth(...) does for a cell, so we can write something like this:
    private void setPreferredWidth(ObservableList<String> items) {
        ListCell<String> cell = new MyListCell();
        double width = 0.0;
        for (String item : items) {
            cell.setText(item);
            width = Math.max(width, cell.prefWidth(-1));
        }
        listView.setPrefWidth(width + listView.getInsets().getLeft() + listView.getInsets().getRight());
    }


We're using only one cell here to measure the widths of all the rows, calculating the maximum width as we go. Since prefWidth() will only work, if the cell has a skin, we have to play a little trick in the constructor of MyListCell:
    private class MyListCell extends ListCell<String> {
        public MyListCell() {
            super();
            updateListView(listView);
            setSkin(createDefaultSkin());
        }
    }


Now everything works the way we want it:

Well almost. If we scroll all the way down, the largest of the rows will require space that is occupied by the vertical scrollbar and thus a horizontal scrollbar is required:

Also the cells we render have an inset that is not yet taken into account. Unfortunately both the scrollbar as well as the (rendered) cell inset will only be available after rendering, so a slight flicker might be visible with the following solution:

import java.util.Random;

import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.layout.GridPane;
import javafx.stage.Popup;
import javafx.stage.Stage;

import org.apache.commons.lang.RandomStringUtils;

public class ListViewWidth extends Application {

    Popup popup = new Popup();
    ListView<String> listView = new ListView<>();
    Button button = new Button("Popup");
    Random rnd = new Random();
    double cellInset = 0.0;

    public ListViewWidth() {
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        grid.add(button, 0, 0);
        button.setOnAction(this::toggle);
        popup.getContent().add(listView);
        Scene scene = new Scene(grid, 640, 480);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void toggle(ActionEvent e) {
        if (popup.isShowing()) {
            popup.hide();
        } else {
            ObservableList<String> items = createItems();
            listView.setItems(items);
            listView.setCellFactory(this::cellFactory);
            Point2D pos = button.localToScreen(0, 0);
            popup.show(button, pos.getX(), pos.getY() + button.getHeight());
            double width = getMaxItemWidth(items) + listView.getInsets().getLeft() + listView.getInsets().getRight() + cellInset;
            ScrollBar bar = getVerticalScrollbar();
            width += bar.getWidth();
            listView.setPrefWidth(width);
        }
    }

    private double getMaxItemWidth(ObservableList<String> items) {
        ListCell<String> cell = new MyListCell();
        double width = 0.0;
        for (String item : items) {
            cell.setText(item);
            width = Math.max(width, cell.prefWidth(-1));
        }
        return width;
    }

    private class MyListCell extends ListCell<String> {
        public MyListCell() {
            super();
            updateListView(listView);
            setSkin(createDefaultSkin());
            insetsProperty().addListener(this::insetsChanged);
        }

        @Override
        protected void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);
            setText(item);
        }
       
        private void insetsChanged(ObservableValue<? extends Insets> observable, Insets oldValue, Insets newValue) {
            cellInset = newValue.getLeft() + newValue.getRight();
        }                   
    }

    private ScrollBar getVerticalScrollbar() {
        ScrollBar result = null;
        for (Node n : listView.lookupAll(".scroll-bar")) {
            if (n instanceof ScrollBar) {
                ScrollBar bar = (ScrollBar) n;
                if (bar.getOrientation().equals(Orientation.VERTICAL)) {
                    result = bar;
                }
            }
        }
        return result;
    }

    private ListCell<String> cellFactory(ListView<String> view) {
        return new MyListCell();
    }

    private ObservableList<String> createItems() {
        ObservableList<String> result = FXCollections.observableArrayList();
        for (int i = 0; i < 140; i++) {
            result.add(RandomStringUtils.random(rnd.nextInt(i + 5) + 1, true, true));
        }
        return result;
    }

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

Wednesday, October 1, 2014

Infinite scrolling with JavaFX TableView

Today I want to show how to realise infinite scrolling with JavaFX TableView.

The idea behind infinite scrolling is to retrieve more data once the user has scrolled to the bottom of the currently loaded data.

To realise this, we need to get the vertical ScrollBar of the TableView like this:
    private ScrollBar getVerticalScrollbar(TableView<?> table) {
        ScrollBar result = null;
        for (Node n : table.lookupAll(".scroll-bar")) {
            if (n instanceof ScrollBar) {
                ScrollBar bar = (ScrollBar) n;
                if (bar.getOrientation().equals(Orientation.VERTICAL)) {
                    result = bar;
                }
            }
        }       
        return result;
    }


Note that this method can only be executed after the table view has been rendered.


With this method we can add a listener to the scrollbars value property:
        ScrollBar bar = getVerticalScrollbar(table);
        bar.valueProperty().addListener(this::scrolled);


The listener can now check whether we are at the bottom and add new data (Persons in our case) to the item list of the table view:
    void scrolled(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
        double value = newValue.doubleValue();
        System.out.println("Scrolled to " + value);
        ScrollBar bar = getVerticalScrollbar(table);
        if (value == bar.getMax()) {
            System.out.println("Adding new persons.");
            double targetValue = value * items.size();
            addPersons();
            bar.setValue(targetValue / items.size());
        }
    }

Note the call to bar.setValue() after data has been added. This is neccessary to keep the view at the elements viewed when reaching the bottom.

Here is the complete example:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Stage;

import org.apache.commons.lang.RandomStringUtils;

public class TableViewInfiniteScrolling extends Application {
   
    ObservableList<Person> items = FXCollections.observableArrayList();
    TableView<Person> table = new TableView<Person>();

    public TableViewInfiniteScrolling() {
        addPersons();
    }

    private void addPersons() {
        for (int i = 0; i < 50; i++) {
            Person p = new Person();
            p.setFirstName(items.size() + " " + RandomStringUtils.random(5, "ABEGHILOUZ"));
            p.setLastName(RandomStringUtils.random(10, "QWRTOUPL"));
            items.add(p);
        }
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        table.setItems(items);
        TableColumn<Person, String> firstNameCol = new TableColumn<Person, String>("First Name");
        firstNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
        TableColumn<Person, String> lastNameCol = new TableColumn<Person, String>("Last Name");
        lastNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));       
        table.getColumns().setAll(firstNameCol, lastNameCol);       
       
        Scene scene = new Scene(table, 400, 400);
        primaryStage.setScene(scene);       
        primaryStage.show();
        ScrollBar bar = getVerticalScrollbar(table);
        bar.valueProperty().addListener(this::scrolled);
    }
   
    private ScrollBar getVerticalScrollbar(TableView<?> table) {
        ScrollBar result = null;
        for (Node n : table.lookupAll(".scroll-bar")) {
            if (n instanceof ScrollBar) {
                ScrollBar bar = (ScrollBar) n;
                if (bar.getOrientation().equals(Orientation.VERTICAL)) {
                    result = bar;
                }
            }
        }       
        return result;
    }
       
    void scrolled(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
        double value = newValue.doubleValue();
        System.out.println("Scrolled to " + value);
        ScrollBar bar = getVerticalScrollbar(table);
        if (value == bar.getMax()) {
            System.out.println("Adding new persons.");
            double targetValue = value * items.size();
            addPersons();
            bar.setValue(targetValue / items.size());
        }
    }
   
    public static void main(String[] args) {
        launch(args);
    }
   
    public class Person {
        private StringProperty firstName;
        public void setFirstName(String value) { firstNameProperty().set(value); }
        public String getFirstName() { return firstNameProperty().get(); }
        public StringProperty firstNameProperty() {
            if (firstName == null) firstName = new SimpleStringProperty(this, "firstName");
            return firstName;
        }
   
        private StringProperty lastName;
        public void setLastName(String value) { lastNameProperty().set(value); }
        public String getLastName() { return lastNameProperty().get(); }
        public StringProperty lastNameProperty() {
            if (lastName == null) lastName = new SimpleStringProperty(this, "lastName");
            return lastName;
        }
    }   
   
}

Wednesday, September 24, 2014

Debugging JavaFX with Linux/X11

When debugging a JavaFX application under Linux there is the Problem of mouse grabbing, i.e. the application under test grabs the mouse and no other application can receive normal GUI events.

This of course makes debugging nearly impossible. After some research I've found a solution in the issue tracker of JavaFX: You have to set the system property (aka VM arguments) -Dglass.disableGrab=true.

Like this in eclipse:

BTW, according to the Java bug database, for AWT applications the same can be achieved by using -Dsun.awt.disablegrab=true but I haven't tried that.

Important caveat:  If you start your application this way, drag and drop will no longer work in your application.

Wednesday, September 17, 2014

JavaFX bindings and tooltips

In this post, I will show two things:
  • How to use JavaFX bindings to set a tooltip
  • How to display tooltips if the widget is disbled
This is our GUI:

Pretty simple so far. Here's the code:

package prv.rli.codetest;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class TooltipOnDisabledButton extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        grid.setHgap(20);

        Button button = new Button("Mystery");
        grid.add(button, 0, 0);
       
        CheckBox checkbox = new CheckBox("enabled");
        grid.add(checkbox, 1, 0);
       
        Scene scene = new Scene(grid);
        primaryStage.setScene(scene);       
        primaryStage.show();
    }
   
    public static void main(String[] args) {
        launch(args);
    }
}


Next, I want the "Mystery" button to be enabled/disabled if the checkbox is checked/unchecked.

Thanks to JavaFX bindings this can be done pretty easily, by adding this line right before the scene is created:

button.disableProperty().bind(checkbox.selectedProperty().not());

What this means is that the disableProperty of the button is now kept in sync with the (negated) selectedProperty of the checkbox.

And now, I also want the Button to display a Tooltip which will show the user how to enable/disable the button. This could be done by installing a ChangeListener to the selectedProperty() of the checkbox, but since we want to show (off) bindings, let's create a binding that will take a BooleanProperty and produce a Tooltip.

The whole thing then looks like this:

package prv.rli.codetest;

import javafx.application.Application;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class TooltipOnDisabledButton extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        grid.setHgap(20);

        Button button = new Button("Mystery");
        grid.add(button, 0, 0);
       
        CheckBox checkbox = new CheckBox("enabled");
        grid.add(checkbox, 1, 0);
       
        button.disableProperty().bind(checkbox.selectedProperty().not());
        button.tooltipProperty().bind(new TooltipBinding(checkbox.selectedProperty()));
       
        Scene scene = new Scene(grid);
        primaryStage.setScene(scene);       
        primaryStage.show();
    }
   
    private static class TooltipBinding extends ObjectBinding<Tooltip> {
           
        BooleanProperty enabled;
       
        public TooltipBinding(BooleanProperty enabled) {
            bind(enabled);
            this.enabled = enabled;
        }

        @Override
        protected Tooltip computeValue() {
            if (enabled.get()) {
                return new Tooltip("To disable this button, uncheck the box to right.");
            }
            else {
                return new Tooltip("To enable this button, check the box to right.");
            }
        }
    }
   
    public static void main(String[] args) {
        launch(args);
    }
}


The TooltipBinding produces a new Tooltip (we could also just create the two tooltips and return them, saving the garbage collector some work) whenever the bound property is changed. 

And it works like a charm -- well almost. When the button is disabled, no tooltip is displayed. The reason for this is documented in [1] and [2]: Disabled nodes are not sent any mouse events.
To circumvent this, we must wrap the button:
        Button button = new Button("Mystery");
        SplitPane wrapper = new SplitPane();
        wrapper.getItems().add(button);
        grid.add(wrapper, 0, 0);

...
        wrapper.tooltipProperty().bind(new TooltipBinding(checkbox.selectedProperty()));

Note that the wrapper has to be a Control in order to have a tooltipProperty(). The problem with this solution is that we now have an additional border (from the SplitPane) around our button. This can easily be amended by


        wrapper.setStyle("-fx-background-color: transparent");

It seems unnatural howeve to introduce a SplitPane as a Tooltip-Wrapper. A simple pane would be the more natural choice. A Pane however has no tooltip property, therefore we must use Tooltip.install() and install a single tooltip whose textProperty() is bound:

package prv.rli.codetest;

import javafx.application.Application;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

public class TooltipOnDisabledButton extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        GridPane grid = new GridPane();
        grid.setHgap(20);

        Button button = new Button("Mystery");
        Pane wrapper = new Pane();
        wrapper.getChildren().add(button);
        grid.add(wrapper, 0, 0);
       
        CheckBox checkbox = new CheckBox("enabled");
        grid.add(checkbox, 1, 0);
       
        button.disableProperty().bind(checkbox.selectedProperty().not());
        Tooltip tooltip = new Tooltip();
        tooltip.textProperty().bind(new TooltipBinding(checkbox.selectedProperty()));
        Tooltip.install(wrapper, tooltip);
       
        Scene scene = new Scene(grid);
        primaryStage.setScene(scene);       
        primaryStage.show();
    }
   
    private static class TooltipBinding extends ObjectBinding<String> {
           
        BooleanProperty enabled;
       
        public TooltipBinding(BooleanProperty enabled) {
            bind(enabled);
            this.enabled = enabled;
        }

        @Override
        protected String computeValue() {
            if (enabled.get()) {
                return "To disable this button, uncheck the box to the right.";
            }
            else {
                return "To enable this button, check the box to the right.";
            }
        }
    }
   
    public static void main(String[] args) {
        launch(args);
    }
}


Note that the TooltipBinding now generates a String instead of a whole tooltip.

Sunday, May 26, 2013

java.util.List.subList StackOverflowError

The other day I came across an interesting problem in one of our products with the program throwing:

Exception in thread "main" java.lang.StackOverflowError
    at java.util.ArrayList$SubList.rangeCheckForAdd(ArrayList.java:1119)
    at java.util.ArrayList$SubList.add(ArrayList.java:963)
    at java.util.ArrayList$SubList.add(ArrayList.java:965)

The code responsible for this looks roughly like this:

    String documentSubType = new String();
    List<String> uids = new ArrayList<String>();
    public void setUid(int index, String value, String documentSubType) {
        while (uids.size() < index + 1) {
            uids.add("");
        }
        uids.set(index, value);
       
        if (documentSubType != null && !documentSubType.isEmpty()) {
            this.documentSubType = documentSubType;
            uids = uids.subList(0, index + 1);
        }
    }

On the whole it looks innocent enough so how comes it still seems to make the stack explode?

The answer lies in the (careless) usage of the subList method. The intention was to set the length of the uids list exactly to the index+1 if certain criteria are met.

Unfortunately subList returns a view on the original list (this is documented) which is implemented in AbstractList by use of a pointer to the parent list (this is not documented). When calling certain operations on such a view, the operations are called recursively on the parent list (this is absolutely not documented).
If you call subList on a view you get a view on a view, meaning you now have a parent hierarchy two levels deep.

If you call the setUid method above a few thousand times you get a parent hierarchy a few thousand levels deep which is then used within a recursive method call and - hey presto - there is your stack overflow.

The following snippet shows the problem isolated:

    public static void main(String[] args) {
        List<String> lst = new ArrayList<String>();
        lst.add(""); 
        for (int i = 0; i < 50000; i++) {

            lst = lst.subList(0, 1);
        }
        lst.add("test2");       
    }

Lessons learned: Do NOT use subList recursively (i.e. on a list that was itself created by a call to subList) because doing so will slow down (many parent pointers to walk through) or crash (StackOverflowError) your program.

Monday, December 17, 2012

The Traps of String.split in Java

I just stumbled across a few peculiarities in the split Method of java.lang.String.

We'll start with a simple example:
    String value = "A:B";
    String[] parts = value.split(":");
    System.out.println("Parts has " + parts.length + " elements: " + Arrays.asList(parts));       
No surprises here. We get the output:
    Parts has 2 elements: [A, B]

Now let's try the empty string:
    String value = "";
    String[] parts = value.split(":");
    ...
Since there is no ":" in the empty string, the whole input string is returned:
    Parts has 1 elements: []
 


This may easily let you jump to the conclusion that the returned array of strings will always at least contain one element, since it returns the input if no delimiter is found. But that is not the case:
    String value = ":";
    String[] parts = value.split(":");
    ...
This will give:

    Parts has 0 elements: []

So even though our input string is now longer, the output array has fewer elements. How comes? Well if we split ":" along ":", we get ["", ""], but split() will not by default include trailing empty strings and thus the array returned is []. This seems counterintuitive to me.

The solution is to use the 2 parameter version of split and pass a negative number as the second parameter:
    String value = ":";
    String[] parts = value.split(":", -1);
    ...
which will give:
    Parts has 2 elements: [, ]

With that in mind, we can easily answer the question what the following code will give us:
    String value = "|";
    String[] parts = value.split("|");
    ...

 Since this looks just like the ":"-example above, it must give us an empty array, right?
Wrong. It gives us:
    Parts has 2 elements: [, |]
Whoa, what has happened? The parameter passed to split is actually treated as a regular expression and "|" is the or-operator of regular expressions, so we try to split the input string at every occurence of "nothing or nothing" which boils down to splitting the input string before any character, which gives us an array containing an empty string and the pipe character.
If you do:
    String value = "abc";
    String[] parts = value.split("|");
you'll get:
    Parts has 4 elements: [, a, b, c]
If you really want to split at pipe characters you have to use split("\\|"), since this will escape the regular expression meaning of the pipe character.

To summarize it, there are two caveats:
  • Use the two parameter version of split(), if you want to avoid getting an empty array.
  • Escape characters with special meaning in regular expressions (e.g. . * + [ ] ^ $)

 










 



 

Sunday, September 4, 2011

The pitfalls of compound assignment operators

Compound assignment operators

You may well know, that in Java (assuming x and y are int variables), instead of writing
    x = x + y;
you can use
    x += y;

The shortcut form is called compound assignment operator and it exists for a lot of operators. They seem harmless enough at first sight.

Now look at this:
    int x = 5, y = 3;
    x *= y + 1;
    System.out.println(x);
You may think that the second line is equivalent to x = x * y + 1, but it is not. It is equivalent to x = x * (y + 1). Therefore x becomes 5 * (3 + 1), i.e. 20.

Short circuit evaluation

Short circuit evaluation describes the fact, that in Java (and many other languages) a boolean expression is only evaluated as long as its final result is not yet known.
    boolean result = true && foo(); // foo gets called
vs.
    boolean result = false && foo(); // foo gets not called
You can (and should) use that to your advantage: Cheap operations to the left, expensive ones to the right (in case of &&, it is the other way round for ||). It also common to use short circuit evaluation for precondition checking:
    boolean result = x != null && x.getMyBool();
If x is really null, we must not call the getMyBool method as it would throw a NullPointerException.

The misleading &= operator

Back to compound assignment operators, there is a &= operator. At first sight, it looks like you could abbreviate
    keep = keep && foo();
by
    keep &= foo();
But, alas, you loose short circuit evaluation, because that really means
    keep = keep & foo();
And the & operator has no short circuit evaluation.

This can be detrimental to the performance of your code, e.g. in cases like this:
   keep &= expensiveOperation1(); 
   keep &= expensiveOperation2(); 
   keep &= expensiveOperation3(); 
   keep &= expensiveOperation4(); 
Or outright fatal, if an operation must not be called
   Object x = null;
   boolean keep = x != null;
   keep &= x.toString().isEmpty();
And there is no &&= compound assignment operator. 

Conclusion

It makes me wonder whether the comfort of saving a few keystrokes and having slightly more compact code to read outweighs these pitfalls and the increased complexity of the language.




Wednesday, July 20, 2011

Removing entries from a Java Collection

Let's look at some simple code:
public static void main(String[] args) {
  Map<String, String> m = new HashMap<String, String>();
  m.put("A", "B"); m.put("C", "D"); m.put("E", "F");
  for (Map.Entry<String, String> e : m.entrySet()) {
    if (e.getValue().equals("D")) {
      m.remove(e.getKey());
    }
  }
} 
After setting up a map which contains three entries (A->B, C->D, E->F), it iterates over its entries and removes an entry, if it matches a certain condition.
Compile, Run, works perfectly (on my machine, with my JDK, etc.)

Now we just change the condition to:
if (e.getValue().equals("B")) {

Compile, Run, oops - we get a ConcurrentModificationException like this:
Exception in thread "main" java.util.ConcurrentModificationException
  at java.util.HashMap$HashIterator.nextEntry(HashMap.java:793)
  at java.util.HashMap$EntryIterator.next(HashMap.java:834)
  at java.util.HashMap$EntryIterator.next(HashMap.java:832)
  at prv.rli.codetest.CodeTest.main(CodeTest.java:11)

Why?
  • The for (x : y) statement is syntactic sugar for the old school collection iteration with an Iterator:
    Iterator<Map.Entry<String, String>> it = m.entrySet().iterator();
    while (it.hasNext()) {
    Map.Entry<String, String> e = it.next();
    }
  • An Iterator may throw a ConcurrentModificationException (a RuntimeException), if you change the collection it iterates over.



But why only with "B"?
  • Pure luck. The HashMap happens to be ordered like this: {E=F, A=B, C=D}, therefore the entry with value D is the last one and since the iterator will return false for hasNext(), we leave the loop, next() is no longer called and nothing happens.
So what is the correct way of removing an element from a map? There are several ways, which we will look at: Using the remove method of the iterator
Iterator<Map.Entry<String, String>> it = m.entrySet().iterator();
  while (it.hasNext()) {
    Map.Entry<String, String> e = it.next();
    if (e.getValue().equals("B")) {
      it.remove();
    }
  }
The Iterator interface provides a remove method, which will remove the element the last call to next() returned. Unfortunately the remove method may not be supported by some iterators. In this case the call to remove() would throw an UnsupportedOperationException. If we cannot use it.remove() we can also remember the keys of elements to remove and remove them after the iteration has completed:
  Set<String> toRemove = new HashSet<String>();
  for (Map.Entry<String, String> e : m.entrySet()) {
    if (e.getValue().equals("B")) {
      toRemove.add(e.getKey());
    }
  }
  m.keySet().removeAll(toRemove);
Note that we can use the for (x : y) notation here. Finally, if you remove most elements of a collection you can also create a new collection and only copy those elements you want to keep:
  Map<String, String> m2 = new HashMap<String, String>();
  for (Map.Entry<String, String> e : m.entrySet()) {
    if (!e.getValue().equals("B")) {
      m2.entrySet().add(e);
    }
  }
  m = m2;

This may have advantages in terms of memory consumption, because the new collection will not waste space for deleted entries.