[23.12.29] Java 21 - Sequenced Collections
이번 주제는 Java 21에 새로운 기능인 Sequenced Collections 에 관한 주제입니다.
출현 배경
list.add(0, "hello");
list.get(0);
list.get(list.size() - 1);
위 예시처럼 컬렉션을 사용하고 있다.
그러나 마지막 또는 첫 번째 요소를 가져오는데 불편한 부분이 있었다.
또 역순으로 가져오기 위해서는 Collections 클래스나 다른 방법을 이용해야한다.
위 불편함을 개발자 친화적으로 만들기 위해서 Java 21 부터는 Sequenced Collection 이 탄생했다.
Sequenced Collection
Sequenced collections, Sequenced set, Sequenced maps 3가지가 추가되었다.
예를 들어 자주 사용하는 List를 보자.
<Java 21 전>
public interface List<E> extends Collection<E> {
<Java 21 후>
public interface List<E> extends SequencedCollection<E> {
상속 클래스가 변경되면서 디폴트 메서드가 추가되었다.
각 메서드는 List 인터페이스에 오버라이드하여 List에 맞게 사용되어지고 있다.
public interface SequencedCollection<E> extends Collection<E> {
SequencedCollection<E> reversed();
default void addFirst(E e) {
throw new UnsupportedOperationException();
}
default void addLast(E e) {
throw new UnsupportedOperationException();
}
default E getFirst() {
return this.iterator().next();
}
default E getLast() {
return this.reversed().iterator().next();
}
default E removeFirst() {
var it = this.iterator();
E e = it.next();
it.remove();
return e;
}
default E removeLast() {
var it = this.reversed().iterator();
E e = it.next();
it.remove();
return e;
}
}
주의 사항은 reverse 메서드는 새로운 인스턴스를 반환하지 않는다.
따라서 list를 수정하는 경우 reversed list도 영향을 받는다.
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.addFirst("hello1");
list.addFirst("hello2");
list.addFirst("hello3");
SequencedCollection<String> reversed = list.reversed();
list.removeFirst();
System.out.println(list); //[hello2, hello1]
System.out.println(reversed); //[hello1, hello2]
}
예시로 LinkedHashSet 도 보자.
<Java 21 전>
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
<Java 21 후>
public class LinkedHashSet<E>
extends HashSet<E>
implements SequencedSet<E>, Cloneable, java.io.Serializable {
SequencedSet = reversed() + Set 이라고 보면 된다. LinkedHashset은 순서를 보장해준다.
그러나 아래와 같이 사용하면 reversed()를 사용할 수 없다.
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<>();
set.add("hello1");
set.add("hello2");
set.add("hello0");
System.out.println(set);
}
사용하기 위해선 아래와 같이 사용하자. Set은 SequnencedSet을 상속받고 있지 않기 때문이다.
그리고 동일하게 reversed에 영향을 받는다. 또 만약 아래 상태에서 추가를하게 된다면 reversed와 기존 set요소의 순서가 바뀔 수 있다.
결과를 참고하자.
public static void main(String[] args) {
SequencedSet<String> set = new LinkedHashSet<>();
set.add("hello1");
set.add("hello2");
set.add("hello3");
SequencedSet<String> reversed = set.reversed();
reversed.add("test");
System.out.println(set); //[hello1, hello2, hello3, test]
System.out.println(reversed); //[test, hello3, hello2, hello1]
}
마지막으로 SequencedMap을 살펴보자.
public interface SequencedMap<K, V> extends Map<K, V> {
SequencedMap<K, V> reversed();
default Map.Entry<K,V> firstEntry() {
var it = entrySet().iterator();
return it.hasNext() ? new NullableKeyValueHolder<>(it.next()) : null;
}
default Map.Entry<K,V> lastEntry() {
var it = reversed().entrySet().iterator();
return it.hasNext() ? new NullableKeyValueHolder<>(it.next()) : null;
}
default Map.Entry<K,V> pollFirstEntry() {
var it = entrySet().iterator();
if (it.hasNext()) {
var entry = new NullableKeyValueHolder<>(it.next());
it.remove();
return entry;
} else {
return null;
}
}
default Map.Entry<K,V> pollLastEntry() {
var it = reversed().entrySet().iterator();
if (it.hasNext()) {
var entry = new NullableKeyValueHolder<>(it.next());
it.remove();
return entry;
} else {
return null;
}
}
default V putFirst(K k, V v) {
throw new UnsupportedOperationException();
}
default V putLast(K k, V v) {
throw new UnsupportedOperationException();
}
default SequencedSet<K> sequencedKeySet() {
class SeqKeySet extends AbstractMap.ViewCollection<K> implements SequencedSet<K> {
Collection<K> view() {
return SequencedMap.this.keySet();
}
public SequencedSet<K> reversed() {
return SequencedMap.this.reversed().sequencedKeySet();
}
public boolean equals(Object other) {
return view().equals(other);
}
public int hashCode() {
return view().hashCode();
}
}
return new SeqKeySet();
}
default SequencedCollection<V> sequencedValues() {
class SeqValues extends AbstractMap.ViewCollection<V> implements SequencedCollection<V> {
Collection<V> view() {
return SequencedMap.this.values();
}
public SequencedCollection<V> reversed() {
return SequencedMap.this.reversed().sequencedValues();
}
}
return new SeqValues();
}
default SequencedSet<Map.Entry<K, V>> sequencedEntrySet() {
class SeqEntrySet extends AbstractMap.ViewCollection<Map.Entry<K, V>>
implements SequencedSet<Map.Entry<K, V>> {
Collection<Map.Entry<K, V>> view() {
return SequencedMap.this.entrySet();
}
public SequencedSet<Map.Entry<K, V>> reversed() {
return SequencedMap.this.reversed().sequencedEntrySet();
}
public boolean equals(Object other) {
return view().equals(other);
}
public int hashCode() {
return view().hashCode();
}
}
return new SeqEntrySet();
}
}
여기서 pollFirstEntry()에 대해서 주의할 부분이 있다.
reversed와 reversed 이전 map은 같은 객체이기에 만약 reversed 이후 map에서 pollFirstEntry를 한다면?
그렇다면 기존 map은 pollLastEntry한 것과 동일하다.
public static void main(String[] args) {
SequencedMap<String, String> map = new LinkedHashMap<>();
map.put("a1", "hello");
map.put("a2", "hello");
map.put("a3", "hello");
SequencedMap<String, String> reversed = map.reversed();
reversed.pollFirstEntry();
System.out.println(map); //{a1=hello, a2=hello}
System.out.println(reversed); //{a2=hello, a1=hello}
}
이렇게 간단하게 새로운 Sequenced Collection 에 대해서 알아봤다.