Observable 컬렉션과 배열

Observable 인터페이스는 5개의 하위 인터페이스가 있다.

• ObservableValue +
• ObservableList  +
• ObservableMap +
• ObservableSet +
• ObservableArray

이들은 javafx.collections 패키지에 있는 관찰가능한 컬렉션과 배열이다. ObservableList와 ObservableMap, ObservableSet, ObservableArray은 java.util의 List, Map, Set을 각각 확장한 것이므로 실제로 자바 컬렉션이다.

관찰가능 기능은 등록한 리스너에 대한 통지 기능이다. 이들은 Observable이므로 InvalidateListener를 등록하면 해당 컬렉션의 내용이 변경될 때마다 통지된다. 관찰가능 컬렉션들은 변경이벤트를 통해 자세한 변경 사항을 전달할 수 있다.

ObservableList 이해

ListChangeListener를 등록하는 메소드

List는 ObservableList의 상위 인터페이스이다. 다음 두 메소드는 ListChangeListeners를 등록하고 해제한다.

• addListener(ListChangeListener<? super E> listener) +
• removeListener(ListChangeListener<? super E> listener)

편의 메소드

• addAll(E... elements) +
• setAll(E... elements) +
• setAll(Collection<? extends E> col) +
• removeAll(E... elements) +
• retainAll(E... elements) +
• remove(int from, int to) +
• filtered(Predicate<E>) +
• sorted(Comparator<E>) +
• sorted() +

filtered()와 sorted()는 ObservableList를 감싼 FilteredList 또는 SortedList를 리턴한다. ObservableList의 값이 변경되면 FilteredList와 SortedList가 변경 사항을 반영한다.

ListChangeListener인터페이스는 onChange(ListChangeListener.Change<? extends E> change) 하나를 정의하며 이 메소드는 ObservableList의 내용이 변경될 때 콜백된다.

Listing 7-1. ObservableListExample.java

public class ObservableListExample {
    public static void main(String[] args) {
        ObservableList<String> strings = FXCollections.observableArrayList();

        strings.addListener((Observable observable) -> {
            System.out.println("\tlist invalidated");
        });

        strings.addListener((Change<? extends String> change) -> {
            System.out.println("\tstrings = " + change.getList());
        });

        System.out.println("Calling add(\"First\"): ");
        strings.add("First");

        System.out.println("Calling add(0, \"Zeroth\"): ");
        strings.add(0, "Zeroth");

        System.out.println("Calling addAll(\"Second\", \"Third\"): ");
        strings.addAll("Second", "Third");

        System.out.println("Calling set(1, \"New First\"): ");
        strings.set(1, "New First");

        final List<String> list = Arrays.asList("Second_1", "Second_2");
        System.out.println("Calling addAll(3, list): ");
        strings.addAll(3, list);

        System.out.println("Calling remove(2, 4): ");
        strings.remove(2, 4);

        final Iterator<String> iterator = strings.iterator();
        while (iterator.hasNext()) {
            final String next = iterator.next();
            if (next.contains("t")) {
                System.out.println("Calling remove() on iterator: ");
                iterator.remove();
            }
        }

        System.out.println("Calling removeAll(\"Third\", \"Fourth\"): ");
        strings.removeAll("Third", "Fourth");
    }
}

자바 컬렉션과은 public API에 List, Map, Set과 같은 인터페이스와 ArrayList, HashMap, HashSet과 같은 구현체를 모두 포함하지만 JavaFX의 관찰가능한 컬렉션 프레임워크는 인터페이스만을 제공한다. JavaFX 컬렉션 객체를 만들려면 FXCollections를 사용한다.

ObservableList<String> strings = FXCollections.observableArrayList();

그리고 리스트에 InvalidationListener와 ListChangeListener를 등록한다. 두 리스너 모두 인자가 하나이므로 람다식으로 매개변수를 지정한다. 무효리스너는 단순히 메시지를 프린트한다. 리스트 변경리스너는 관찰가능한 리스트의 내용을 프린트한다.

실제로 실행하면, 코드에서 관찰가능 리스트의 내용을 변경하면 무효 리스너와 변경 리스너 모두에 대한 콜백을 트리거한다.

무효리스너 또는 리스트 변경리스너의 인스턴스가 이미 등록되었다면 이후에 그 리스너를 addListener()로 등록해도 무시된다. 물론 여러가지 다른 무효리스너와 리스트 변경리스너를 등록할 수는 있다.

ListChangeListener에서 변경이벤트 처리

ListChangeListener.Change클래스와 OnChange()콜백 메소드로 변경이벤트를 처리하는 방법을 다룬다. 앞에서 FXCollections.observableArrayList()로 만든 ObservableList에 대한 모든 변경에 대해 각 등록된 관찰자에게 변경 이벤트가 생성된다. 각 이벤트 객체는 ListChangeListener.Change 인터페이스를 구현한 인스턴스로서 하나 이상의 고유한 변경을 표현한다.

• boolean next()
• void reset()
• boolean wasAdded()
• boolean wasRemoved()
• boolean wasReplaced()
• boolean wasPermutated()
• int getFrom()
• int getTo()
• int getAddedSize()
• List<E> getAddedSublist()
• int getRemovedSize()
• List<E> getRemoved()
• int getPermutation(int i)
• ObservableList<E> getList()

next()와 reset()은 이벤트 객체에서의 모든 개별적인 변경을 순회하는 커서를 제어한다. onChange()메소드의 엔트리에서 커서는 첫번째 변경 앞에 놓인다. 커서를 첫번째 변경으로 이동하려면 next()를 호출해야 한다. next()를 다시 부르면 나머지 변경으로 커서가 이동한다. 다음 변경에 닿으면 true를 리턴한다. 커서가 마지막 변경에 와 있다면 false를 리턴한다.

변경 종류를 결정하였다면 다른 메소드로 좀 더 자세한 정보를 얻을 수 있다. 또한 getFrom()은 관찰 가능한 리스트에 새로 추가된 원소의 인덱스를 리턴한다. getTo()는 추가된 원소의 끝에서 하나 앞의 원소의 인덱스를 리턴한다.

Listing 7-2. ListChangeEventExample.java

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;

public class ListChangeEventExample {
    public static void main(String[] args) {
        ObservableList<String> strings = FXCollections.observableArrayList();
        strings.addListener(new MyListener());

        System.out.println("Calling addAll(\"Zero\", \"One\", \"Two\", \"Three\"): ");
        strings.addAll("Zero", "One", "Two", "Three");

        System.out.println("Calling FXCollections.sort(strings): ");
        FXCollections.sort(strings);

        System.out.println("Calling set(1, \"Three_1\"): ");
        strings.set(1, "Three_1");

        System.out.println("Calling setAll(\"One_1\", \"Three_1\", \"Two_1\", \"Zero_1\"): ");
        strings.setAll("One_1", "Three_1", "Two_1", "Zero_1");

        System.out.println("Calling removeAll(\"One_1\", \"Two_1\", \"Zero_1\"): ");
        strings.removeAll("One_1", "Two_1", "Zero_1");
    }

    private static class MyListener implements ListChangeListener<String> {
        @Override
        public void onChanged(Change<? extends String> change) {
            System.out.println("\tlist = " + change.getList());
            System.out.println(prettyPrint(change));
        }

        private String prettyPrint(Change<? extends String> change) {
            StringBuilder sb = new StringBuilder("\tChange event data:\n");
            int i = 0;
            while (change.next()) {
                sb.append("\t\tcursor = ")
                    .append(i++)
                    .append("\n");

                final String kind =
                    change.wasPermutated() ? "permutated" :
                        change.wasReplaced() ? "replaced" :
                            change.wasRemoved() ? "removed" :
                                change.wasAdded() ? "added" : "none";

                sb.append("\t\tKind of change: ")
                    .append(kind)
                    .append("\n");

                sb.append("\t\tAffected range: [")
                    .append(change.getFrom())
                    .append(", ")
                    .append(change.getTo())
                    .append("]\n");

                if (kind.equals("added") || kind.equals("replaced")) {
                    sb.append("\t\tAdded size: ")
                        .append(change.getAddedSize())
                        .append("\n");
                    sb.append("\t\tAdded sublist: ")
                        .append(change.getAddedSubList())
                        .append("\n");
                }

                if (kind.equals("removed") || kind.equals("replaced")) {
                    sb.append("\t\tRemoved size: ")
                        .append(change.getRemovedSize())
                        .append("\n");
                    sb.append("\t\tRemoved: ")
                        .append(change.getRemoved())
                        .append("\n");
                }

                if (kind.equals("permutated")) {
                    StringBuilder permutationStringBuilder = new StringBuilder("[");
                    for (int k = change.getFrom(); k < change.getTo(); k++) {
                        permutationStringBuilder.append(k)
                            .append("->")
                            .append(change.getPermutation(k));
                        if (k < change.getTo() - 1) {
                            permutationStringBuilder.append(", ");
                        }
                    }
                    permutationStringBuilder.append("]");
                    String permutation = permutationStringBuilder.toString();
                    sb.append("\t\tPermutation: ").append(permutation).append("\n");
                }
            }
            return sb.toString();
        }
    }
}

FXCollections의 Factory와 Utility 메소드

Listing 7-6. FXCollectionsExample.java

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;

import java.util.Arrays;
import java.util.Comparator;
import java.util.Random;

public class FXCollectionsExample {
    public static void main(String[] args) {
        ObservableList<String> strings = FXCollections.observableArrayList();
        strings.addListener(new MyListener());

        System.out.println("Calling addAll(\"Zero\", \"One\", \"Two\", \"Three\"): ");
        strings.addAll("Zero", "One", "Two", "Three");

        System.out.println("Calling copy: ");
        FXCollections.copy(strings, Arrays.asList("Four", "Five"));

        System.out.println("Calling replaceAll: ");
        FXCollections.replaceAll(strings, "Two", "Two_1");

        System.out.println("Calling reverse: ");
        FXCollections.reverse(strings);

        System.out.println("Calling rotate(strings, 2): ");
        FXCollections.rotate(strings, 2);

        System.out.println("Calling shuffle(strings): ");
        FXCollections.shuffle(strings);

        System.out.println("Calling shuffle(strings, new Random(0L)): ");
        FXCollections.shuffle(strings, new Random(0L));

        System.out.println("Calling sort(strings): ");
        FXCollections.sort(strings);

        System.out.println("Calling sort(strings, c) with custom comparator: ");
        FXCollections.sort(strings, new Comparator<String>() {
            @Override
            public int compare(String lhs, String rhs) {
                // Reverse the order
                return rhs.compareTo(lhs);
            }
        });

        System.out.println("Calling fill(strings, \"Ten\"): ");
        FXCollections.fill(strings, "Ten");
    }

    // We omitted the nested class MyListener, which is the same as in Listing 7-2
}