Channels ▼

Eric Bruno

Dr. Dobb's Bloggers

Simple Searching in Java

March 22, 2012

I recently worked on a GUI project that included a long list of item names within a list box. Since the list wasn't sorted alphabetically, but instead by popularity, it was difficult to find items by name. To resolve this, I allowed the user to enter search text, and I actively filtered the list with each keystroke. I wrote code like the following to loop through the list of entries on each keystroke as the user typed, and filtered out the entries that didn't contain the text the user entered:

    public void handleSearchByKey(String oldVal, String newVal) {
        // If the number of characters in the text box is less than last time
        // it must be because the user pressed delete
        if ( oldVal != null && (newVal.length() < oldVal.length()) ) {
            // Restore the lists original set of entries 
            // and start from the beginning
            list.setItems( entries );
        }
        
        // Change to upper case so that case is not an issue
        newVal = newVal.toUpperCase();

        // Filter out the entries that don't contain the entered text
        ObservableList<String> subentries = FXCollections.observableArrayList();
        for ( Object entry: list.getItems() ) {
            String entryText = (String)entry;
            if ( entryText.toUpperCase().contains(newVal) ) {
                subentries.add(entryText);
            }
        }
        list.setItems(subentries);
    }

The result was quite dramatic, although the code is actually very simple. Here are the steps (not in order):

  • Loop through all the items in the list
  • Create a sublist of items, where each item contains the text entered
  • If the user hits the backspace key, restore the list to its full set of original entries, and search from the top again
  • Replace list's entries with the sublist just created

As you type, you continue to refine the search, and the list entries become fewer. For instance, assume the list contains the following four entries:

Eric J. Bruno
Joseph Bruno
Ashley Bruno
Brandon Bruno

If you type "j," the list will contain the following items:

Eric J. Bruno
Joseph Bruno

However, if you follow with another key press so the search text is now "jo," you will have only one entry as you'd expect:

Joseph Bruno

Of course you wouldn't really need to search this way for such a short list, but remember this is only an example. However, this code has one flaw: it's not flexible enough to enter your search terms out of order. For instance, to find "Joseph Bruno" in the list, you cannot type "bruno j" as your search term. This is important if you're looking through a list of entries that contain multiple words, but you're not entirely sure of the spellings or combinations of words.

A Google-Like Improvement

Let's improve our search to be more Google-like, or at least more flexible in terms of the ordering of search terms:

    public void handleSearchByKey(String oldVal, String newVal) {
        // If the number of characters in the text box is less than last time
        // it must be because the user pressed delete
        if ( oldVal != null && (newVal.length() < oldVal.length()) ) {
            // Restore the lists original set of entries 
            // and start from the beginning
            list.setItems( entries );
        }
        
        // Break out all of the parts of the search text 
        // by splitting on white space
        String[] parts = newVal.toUpperCase().split(" ");

        // Filter out the entries that don't contain the entered text
        ObservableList<String> subentries = FXCollections.observableArrayList();
        for ( Object entry: list.getItems() ) {
            boolean match = true;
            String entryText = (String)entry;
            for ( String part: parts ) {
                // The entry needs to contain all portions of the
                // search string *but* in any order
                if ( ! entryText.toUpperCase().contains(part) ) {
                    match = false;
                    break;
                }
            }

            if ( match ) {
                subentries.add(entryText);
            }
        }
        list.setItems(subentries);
    }

The change here is again simple: Break the search text into parts separated by spaces. The split() method of the String class accomplishes this. The result is an array containing the individual words in the overall search text. Next, loop over the entries in the list, and if an entry contains each of the words, it gets to stay in the list. Since it's a loop, the ordering of the individual words doesn't matter.

As a result, you can now type "bruno j," and the list will contain the following entries:

Eric J. Bruno
Joseph Bruno

Why? Because both entries contain "bruno" and "j." Change the search text to "bruno jo" and you will refine the list down to a single entry, as expected: "Joseph Bruno." By the way, this would have yielded no results with the first implementation. It is clearly an improvement!

Again, this is a simplified example of a real-world scenario where it can often be helpful to provide flexibility when searching through long lists of text. And, this flexibility need not be difficult to implement. The code for a full JavaFX 2.0 example (written in pure Java, of course) is below. Run it yourself to see it in action.

Happy coding!

— EJB

package javafxapplication1;

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

/**
 * @author ericjbruno
 */
public class JavaFXApplication1 extends Application {
    ObservableList<String> entries = FXCollections.observableArrayList();    
    ListView list = new ListView();

    public static void main(String[] args) {
        launch(args);
    }
    
    @Override
    public void start(Stage primaryStage) {
        primaryStage.setTitle("Simple Search");
        Button btn = new Button();
        btn.setText("Choose");
        btn.setOnAction(new EventHandler<ActionEvent>() {
            @Override public void handle(ActionEvent event) {
                System.exit(0);
            }
        });
        
        TextField txt = new TextField();
        txt.setPromptText("Search");
        txt.textProperty().addListener(
            new ChangeListener() {
                public void changed(ObservableValue observable, 
                                    Object oldVal, Object newVal) {
                    handleSearchByKey2((String)oldVal, (String)newVal);
                }
            });
        
        // Set up the ListView
        list.setMaxHeight(180);
        // Populate the list's entries
        for ( int i = 0; i < 100; i++ ) {
            entries.add("Item " + i);
        }
        entries.add("Eric J. Bruno");
        entries.add("Joseph Bruno");
        entries.add("Ashley Bruno");
        entries.add("Brandon Bruno");
        list.setItems( entries );
        
        VBox root = new VBox();
        root.setPadding(new Insets(10,10,10,10));
        root.setSpacing(2);
        root.getChildren().addAll(txt,list,btn);
        primaryStage.setScene(new Scene(root, 300, 250));
        primaryStage.show();
    }
    
    public void handleSearchByKey(String oldVal, String newVal) {
        // If the number of characters in the text box is less than last time
        // it must be because the user pressed delete
        if ( oldVal != null && (newVal.length() < oldVal.length()) ) {
            // Restore the lists original set of entries 
            // and start from the beginning
            list.setItems( entries );
        }
        
        // Change to upper case so that case is not an issue
        newVal = newVal.toUpperCase();

        // Filter out the entries that don't contain the entered text
        ObservableList<String> subentries = FXCollections.observableArrayList();
        for ( Object entry: list.getItems() ) {
            String entryText = (String)entry;
            if ( entryText.toUpperCase().contains(newVal) ) {
                subentries.add(entryText);
            }
        }
        list.setItems(subentries);
    }

    public void handleSearchByKey2(String oldVal, String newVal) {
        // If the number of characters in the text box is less than last time
        // it must be because the user pressed delete
        if ( oldVal != null && (newVal.length() < oldVal.length()) ) {
            // Restore the lists original set of entries 
            // and start from the beginning
            list.setItems( entries );
        }
        
        // Break out all of the parts of the search text 
        // by splitting on white space
        String[] parts = newVal.toUpperCase().split(" ");

        // Filter out the entries that don't contain the entered text
        ObservableList<String> subentries = FXCollections.observableArrayList();
        for ( Object entry: list.getItems() ) {
            boolean match = true;
            String entryText = (String)entry;
            for ( String part: parts ) {
                // The entry needs to contain all portions of the
                // search string *but* in any order
                if ( ! entryText.toUpperCase().contains(part) ) {
                    match = false;
                    break;
                }
            }

            if ( match ) {
                subentries.add(entryText);
            }
        }
        list.setItems(subentries);
    }
}

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Comments:

ubm_techweb_disqus_sso_-f5ce610729868e3c0f7c0ddd6b5f09ce
2012-05-08T17:50:39

Thanks for the article. I only glanced over it, so maybe I misunderstood your intention - if not, I think a trie performs faster prefix searches.


Permalink
ubm_techweb_disqus_sso_-34a114d9e729bf997f974e49925be201
2012-04-03T20:03:45

That's an excellent improvement. Thank you!


Permalink
ubm_techweb_disqus_sso_-f5556c08afb65764ed6cd9f93eab3cf0
2012-04-03T19:30:24

Works in English, but case conversion is locale-dependent. Here's a note in the String.toUpperCase javadoc:

Note: This method is locale sensitive, and may produce unexpected results if used for strings that are intended to be interpreted locale independently. Examples are programming language identifiers, protocol keys, and HTML tags. For instance, "title".toUpperCase() in a Turkish locale returns "T?TLE", where '?' is the LATIN CAPITAL LETTER I WITH DOT ABOVE character. To obtain correct results for locale insensitive strings, use toUpperCase(Locale.ENGLISH).


Permalink


Video