순회
자료구조에서 순회는 자료구조에 들어있는 데이터를 차례대로 접근해서 처리하는 것을 말한다.
다양한 자료구조가 존재하며 각각의 자료구조마다 데이터를 접근하는 방법은 모두 다르다.
하지만 자료구조의 구현과 관계 없이 모든 자료구조를 동일한 방법으로 순회할 수 있는 일관성 있는 방법이 있다면 자료구조를 사용하는 개발자 입장에서는 매우 편리할 것이다.
자바는 이런 문제를 해결하기 위해 Iterable
과 Iterator
인터페이스를 제공한다.
✔️Iterable, Iterator
public interface Iterable<T> {
Iterator<T> iterator();
}
public interface Iterator<E> {
boolean hasNext();
E next();
}
public class MyArrayIterator implements Iterator<Integer> {
private int currentIndex = -1;
private int[] targetArr;
public MyArrayIterator(int[] targetArr) {
this.targetArr = targetArr;
}
// 다음 항목이 있는지 검사한다.
@Override
public boolean hasNext() {
return currentIndex < targetArr.length - 1;
}
// 다음 항목을 반환한다.
@Override
public Integer next() {
return targetArr[currentIndex++];
}
}
public class MyArray implements Iterable<Integer> {
private int[] array;
public MyArray(int[] array) {
this.array = array;
}
@Override
public Iterator<Integer> iterator() {
return new MyArrayIterator(array);
}
}
for-each문이라 불리는 향상된 for문은 자료구조를 순회하는 것이 목적이다. 자바는 Iterable
인터페이스를 구현한 객체에 대해 향상된 for문을 사용할 수 있게 해준다.
자바 컬렉션 프레임워크는 다양한 자료구조를 제공한다.
자바는 컬렉션 프레임워크를 사용하는 개발자가 편리하고 일관된 방법으로 자료구조를 순회할 수 있도록 Iterable
인터페이스를 제공하고 이미 각각의 구현체에 맞는 Iterator
도 다 구현해두었다.
자바 Collection
인터페이스 상위에 Iterable
이 있다는 것은 모든 컬렉션을 Iterable
, Iterator
로 순회할 수 있다는 것을 말한다.
Map
의 경우 키-값 구조이기 때문에 바로 순회를 할 수 없다. 대신 키나 값을 정해서 순회할 수 있는데 이 때, keySet()
이나 values()
를 사용한다. 물론 Entry
를 반환하는 entrySet()
역시 순회가 가능하다.
public class JavaIterableMain {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
printAll(list.iterator());
foreach(list);
Set<Integer> set = new HashSet<>();
set.add(1);
set.add(2);
set.add(3);
printAll(set.iterator());
foreach(set);
}
private static void foreach(Iterable<Integer> iterable) {
System.out.println("iterable : " + iterable.getClass());
for (Integer integer : iterable) {
System.out.println("integer : " + integer);
}
}
private static void printAll(Iterator<Integer> iterator) {
System.out.println("iterator : " + iterator.getClass());
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
❗Iterator(반복자) 디자인 패턴 : 객체 지향 프로그래밍에서 컬렉션 요소들을 순회할 때 사용되는 디자인 패턴이다. 이 패턴은 컬렉션 내부 표현 방식을 노출하지 않으면서 그 안의 각 요소에 순차적으로 접근할 수 있게 해준다.
Iterator
패턴은 컬렉션 구현과는 독립적으로 요소들을 탐색할 수 있는 방법을 제공하며, 이로 인해 코드 복잡성을 줄이고 재사용성을 높일 수 있다.
public class Main {
public static void main(String[] args) {
Integer int1 = Integer.valueOf(1);
Integer int2 = Integer.valueOf(2);
Integer int3 = Integer.valueOf(3);
int i1 = int1.compareTo(3);
int i2 = int2.compareTo(1);
int i3 = int3.compareTo(3);
System.out.println("i1 = " + i1);
System.out.println("i2 = " + i2);
System.out.println("i3 = " + i3);
}
}
객체의 비교1 - Arrays.sort(array) : 기본 정렬(오름차순)
객체의 비교2 - Arrays.sort(array) : 특정 기준 정렬
public class MyUser implements Comparable<MyUser> {
private String id;
private int age;
public MyUser(String id, int age) {
this.id = id;
this.age = age;
}
// 나이 오름차순 정렬 구현해보기
@Override
public int compareTo(MyUser myUser) {
return (this.age < myUser.age) ? -1 : ((this.age == myUser.age) ? 0 : 1);
}
@Override
public String toString() {
return "MyUser{" +
"id='" + id + '\'' +
", age=" + age +
'}';
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class SortMain3 {
public static void main(String[] args) {
MyUser myUser1 = new MyUser("a", 30);
MyUser myUser2 = new MyUser("b", 20);
MyUser myUser3 = new MyUser("c", 10);
MyUser[] array = {myUser1, myUser2, myUser3};
System.out.println("정렬 전 데이터");
System.out.println(Arrays.toString(array));
System.out.println("정렬 후 데이터");
Arrays.sort(array);
System.out.println(Arrays.toString(array));
}
}
실행 결과
정렬 전 데이터
[MyUser{id='a', age=30}, MyUser{id='b', age=20}, MyUser{id='c', age=10}]
정렬 후 데이터
[MyUser{id='c', age=10}, MyUser{id='b', age=20}, MyUser{id='a', age=30}]
❗객체의 비교 기준이 없는 경우 발생하는 예외 : java.lang.ClassCastException: class collection.compare.MyUser cannot be cast to class java.lang.Comparable
❗해결1 : Comparable을 구현하여 해결하는 경우
How to use the Comparable CompareTo on Strings in Java
public class MyUser1 implements Comparable<MyUser1> {
private String id;
private int age;
public MyUser1(String id, int age) {
this.id = id;
this.age = age;
}
@Override
public String toString() {
return "MyUser{" +
"id='" + id + '\'' +
", age=" + age +
'}';
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int compareTo(MyUser1 o) {
return this.getId().compareTo(o.getId());
}
}
public class SortMain3 {
public static void main(String[] args) {
MyUser1 myUser1 = new MyUser1("a", 30);
MyUser1 myUser2 = new MyUser1("b", 20);
MyUser1 myUser3 = new MyUser1("c", 10);
MyUser1[] array = {myUser3, myUser2, myUser1};
System.out.println("정렬 전 데이터");
System.out.println(Arrays.toString(array));
System.out.println("정렬 후 데이터");
Arrays.sort(array);
System.out.println(Arrays.toString(array));
}
}
실행 결과
정렬 전 데이터
[MyUser{id='c', age=10}, MyUser{id='b', age=20}, MyUser{id='a', age=30}]
정렬 후 데이터
[MyUser{id='a', age=30}, MyUser{id='b', age=20}, MyUser{id='c', age=10}]
❗해결2 : Comparator을 구현하여 해결하는 경우
public class MyUser2 {
private String id;
private int age;
public MyUser2(String id, int age) {
this.id = id;
this.age = age;
}
@Override
public String toString() {
return "MyUser{" +
"id='" + id + '\'' +
", age=" + age +
'}';
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
public class IdComparator implements Comparator<MyUser2> {
@Override
public int compare(MyUser2 o1, MyUser2 o2) {
return (o1.getId().compareTo(o2.getId()));
}
}
public class SortMain4 {
public static void main(String[] args) {
MyUser2 myUser1 = new MyUser2("a", 30);
MyUser2 myUser2 = new MyUser2("b", 20);
MyUser2 myUser3 = new MyUser2("c", 10);
MyUser2[] array = {myUser3, myUser2, myUser1};
System.out.println("정렬 전 데이터");
System.out.println(Arrays.toString(array));
System.out.println("정렬 후 데이터");
Arrays.sort(array, new IdComparator());
System.out.println(Arrays.toString(array));
}
}
실행 결과
정렬 전 데이터
[MyUser{id='c', age=10}, MyUser{id='b', age=20}, MyUser{id='a', age=30}]
정렬 후 데이터
[MyUser{id='a', age=30}, MyUser{id='b', age=20}, MyUser{id='c', age=10}]
Collections.sort(list)
리스트는 순서가 있는 컬렉션이므로 정렬할 수 있다.
이 메서드를 사용하면 기본 정렬이 적용된다.
하지만 이 방식보다는 객체 스스로 정렬 메서드를 가지고 있는 list.sort()
사용을 더 권장
list.sort(null)
별도의 비교자가 없으므로 Comparable
로 비교해서 정렬한다.
자연적 순서로 비교한다.
Collections.sort(list, new IdCompartor())
별도의 비교자로 비교하고 싶다면 두 번째 인자에 비교자를 넘긴다.
하지만 이 방식보다는 객체 스스로 정렬 메서드를 가지고 있는 list.sort()
사용을 더 권장
list.sort(new IdCompartor())
전달한 비교자로 비교한다.
public class CollectionsSortMain {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
Integer max = Collections.max(list);
Integer min = Collections.min(list);
System.out.println("min = " + min);
System.out.println("max = " + max);
System.out.println("list = " + list);
Collections.shuffle(list);
System.out.println("list = " + list);
Collections.sort(list);
System.out.println("list = " + list);
Collections.reverse(list);
System.out.println("list = " + list);
}
}
✔️불변 컬렉션 생성
public class OfMain {
public static void main(String[] args) {
// 편리한 불변 컬렉션 생성(List.of 사용 권장)
List<Integer> list = List.of(1, 2, 3);
Set<Integer> set = Set.of(1, 2, 3);
Map<String, Integer> map = Map.of("One", 1, "Two", 2, "Three", 3);
System.out.println("map = " + map);
System.out.println("set = " + set);
System.out.println("list = " + list);
System.out.println(map.getClass()); // class java.util.ImmutableCollections$MapN
System.out.println(set.getClass()); // class java.util.ImmutableCollections$SetN
System.out.println(list.getClass()); // class java.util.ImmutableCollections$ListN
}
}
❗불변 컬렉션은 말 그대로 불변이므로 변경이 불가능하다. 변경을 시도하게 되면 다음과 같은 예외가 발생한다. → Exception in thread "main" java.lang.UnsupportedOperationException
new ArrayList<>()
사용Collections.unmodifiableList()
사용public class ImmutableMain {
public static void main(String[] args) {
List<Integer> list = List.of(1, 2, 3);
System.out.println("list = " + list);
System.out.println(list.getClass());
List<Integer> mutableList = new ArrayList<>(list);
mutableList.add(4);
System.out.println("mutableList = " + mutableList);
System.out.println(mutableList.getClass());
List<Integer> unmodifiableList = Collections.unmodifiableList(mutableList);
System.out.println(unmodifiableList.getClass());
}
}
실행 결과
list = [1, 2, 3]
class java.util.ImmutableCollections$ListN
mutableList = [1, 2, 3, 4]
class java.util.ArrayList
class java.util.Collections$UnmodifiableRandomAccessList
✔️빈 리스트 생성
public class EmptyListMain {
public static void main(String[] args) {
// 빈 가변 리스트
ArrayList<Integer> list1 = new ArrayList<>();
LinkedList<Integer> list2 = new LinkedList<>();
System.out.println(list1.getClass());
System.out.println(list2.getClass());
// 빈 불변 리스트
List<Integer> list3 = Collections.emptyList();
List<Integer> list4 = List.of();
System.out.println(list3.getClass());
System.out.println(list4.getClass());
}
}
public class SyncMain {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
System.out.println("list class = " + list.getClass());
List<Integer> synchronizedList = Collections.synchronizedList(list);
System.out.println(synchronizedList.getClass());
}
}
실무 선택 가이드
List
: ArrayList
Set
: HashSet
Map
: HashMap
Queue
: ArrayDeque