💡스트림
스트림은 데이터 소스로부터 데이터를 탐색하고 조작하는 도구로, 컬렉션과 배열 등의 데이터를 더 쉽고 효율적으로 다룰 수 있게 도와준다.
스트림을 사용하면 컬렉션 또는 배열의 요소들을 한 번에 하나씩 처리할 수 있다.
스트림을 이용한 표현으로 list.stream().forEach()와 같이 쓸 수 있는데, 여기서 forEach() 같은 메소드를 파이프(Pipe)라고 부른다.
배열(컬렉션) 탐색
for문
for (int i=0; i<list.size(); i++) {
System.out.print(list.get(i) + " ");
}
향상된 for문
for (String word : list) {
System.out.print(word + " ");
}
Iterator
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
System.out.print(iter.next() + " ");
}
Stream
Stream<String> stream = list.stream();
Consumer<String> c1 = str -> System.out.print(str + " ");
스트림을 얻어오는 방법
배열로부터 얻어오는 방법
int[] nums1 = { 10, 20, 30 };
Arrays.stream(nums1).forEach(num -> System.out.println(num));
System.out.println();
컬렉션으로부터 얻어오는 방법
ArrayList<Integer> nums2 = new ArrayList<Integer>();
nums2.add(100);
nums2.add(200);
nums2.add(300);
nums2.stream().forEach(num -> System.out.println(num));
System.out.println();
숫자범위로부터 얻어오는 방법
IntStream.range(1, 10).forEach(num -> System.out.println(num));
Stream<Integer>: 범용 스트림 (forEach > Consumer<Integer>)
IntStream: 전용 스트림 (forEach > IntConsumer)
파일로부터 얻어오는 방법
try {
Path dir = Paths.get("C:\\class\\dev\\eclipse");
Files.list(dir).forEach(p -> {
System.out.println(p.getFileName());
System.out.println(p.toFile().isFile());
System.out.println();
});
} catch (Exception e) {
System.out.println("at Ex74_Stream.m4");
e.printStackTrace();
}
- Paths.get("C:\\Class\\java\\code\\JavaTest\\data\\numbr.txt")
- Paths.get(".\\data\\number.txt")
위의 두 경로는 길이가 다르지만 가리키는 경로는 동일하다. 이런 게 가능한 이유는 경로를 생각하게 해주는 예약어 때문이다.
상대 절대 경로
'.'은 '현재 실행파일이 있는 폴더'를 나타내는 예약어(약속)로, 주소 경로를 짧게 해준다.
자바 폴더에서의 '.'은 프로젝트의 루트 폴더를 가리킨다.
디렉토리로부터 얻어오는 방법
try {
Path dir = Paths.get("C:\\class\\dev\\eclipse");
Files.list(dir).forEach(p -> {
System.out.println(p.getFileName());
System.out.println(p.toFile().isFile());
System.out.println();
});
} catch (Exception e) {
System.out.println("at Ex74_Stream.m4");
e.printStackTrace();
}
💡파이프
파이프(Pipe)는 스트림 객체의 메소드로서 데이터를 연결하는 역할을 하며, 중간 파이프와 최종 파이프로 구분한다.
중간 파이프
중간 파이프는 스트림의 요소들을 중간 단계에서 변환 또는 필터링을 할 때 사용한다.
중간파이프는 반환값이 있으며, 스트림을 반환한다. 이때 반환한 스트림을 다른 파이프나 최종 파이프로 연결할 수 있다.
중간 파이프에는 filter(), distinct(), map(), sorted() 등의 메소드가 있다. 이 메소드는 조건에 따라 요소를 걸러내고, Predicate 인터페이스를 활용하여 조건을 지정할 수 있다.
필터링
필터링을 할 때에는 filter() 중간 파이프 메소드가 사용된다.
Predicate의 특성이 받은 요소를 가지고 true, false를 판단하는 역할을 제공하므로, Predicate가 필터링에 사용되고 있다.
최종 파이프
최종 파이프는 스트림의 요소들에 대해 최종 처리 작업을 수행하며, 최종 파이프는 메소드에 따라 반환값이 없을 수도, 반환값을 가질 수도 있다.
최종 파이프로는 forEach(), count() 등의 메소드가 있다. 이 메소드는 스트림의 각 요소에 대해 지정된 작업을 수행하며, 결과적으로 스트림을 소비한다.
요소 처리
forEach() 메소드 등의 최종 파이프를 사용할 때, 요소를 처리한다고 말한다.
💡중간 파이프 메소드
filter() 메소드
List<Integer> list = Data.getIntList(20);
System.out.println(list); // [36, 12, 36, 86, 24, 1, 32, 65, 85, 86, 78, 16, 57, 7, 65, 34, 11, 37, 99, 12]
위 데이터 목록에 filter() 메소드를 적용해보도록 하자.
짝수만 출력하는 기능을 만든다고 할 때, 반복문, 스트림, 파이프까지 3가지 방법을 이용할 수 있다.
// 반복문
for (int n : list) {
if (n % 2 == 0) {
System.out.printf("%4d", n); // 36 12 36 86 24 32 86 78 16 34 12
}
}
// 스트림
list.stream().forEach(num -> {
if (num % 2 == 0) {
System.out.printf("%-4d", num); // 36 12 36 86 24 32 86 78 16 34 12
}
});
// filter
list.stream().filter(num -> num % 2 == 0).forEach(num -> {
System.out.printf("%-4d", num);
});
filter() 메소드는 filter(num -> false)로 작성되어 숫자 20개를 전달받고 검사를 한다.
중간에 있는 파이프를 거쳐 최종 파이프까지 숫자가 전달되는 과정의 중간에서 데이터를 걸러낸다고 해서 중간 파이프라고 한다.
Data.getUserList().stream()
// .filter(user -> user.getWeight() >= 70 && user.getGender() == 1)
.filter(user -> user.getWeight() >= 70)
.filter(user -> user.getGender() == 1)
.filter(user -> user.getHeight() >= 180)
.forEach(user -> System.out.println(user));
System.out.println();
유저 리스트에서 몸무게가 1이고 성별이 남자이고 키가 180 이상인 사람만 출력한다고 할 때, 조건을 And연산자(&&)로 연결해 줄 수도 있지만, filter를 여러 개 작성할 수도 있다.
distinct() 메소드
필터는 조건부를 만족하는 것만 가능하지, 중복을 걸러내지는 못하므로 중복값을 제거할 때에는 distinct() 메소드를 사용해야 한다.
distinct() 메소드는 앞의 스트림에서 중복 요소를 제거하고 유일한 요소만 남은 새로운 스트림을 다시 반환한다.
List<Integer> list = Data.getIntList();
System.out.println(list.size()); // 100
위의 배열에서 중복값을 제거해 보도록 하자.
Case 1. Set을 이용한 중복 제거
Set<Integer> set1 = new HashSet<Integer>();
for (int n : list) {
set1.add(n); // 중복값 제거
}
System.out.println(set1.size()); // 61
배열에서 100개에서 중복값을 제거하니 61개만 남았다.
Case 2. Set의 생성자를 이용한 중복 제거
Set<Integer> set2 = new HashSet<Integer>(list);
System.out.println(set2.size()); // 61
이전에 Set 배열의 데이터를 정렬하고 싶은데, Set의 특성 때문에 정렬을 못 하므로, 정렬할 수 있는 ArrayList로 바꿔서 정렬하였다.
이번에는 그 반대로 생성자에 list를 넣으면서 ArrayList를 Set으로 변환한 것이다. 이때 Set은 중복이 허용되지 않으므로 자동으로 중복값이 제거된다.
Case 3. distinct를 이용한 중복 제거
System.out.println(list.stream().distinct().count()); // 61
중복을 제거한 뒤에 개수를 찍어야 하는데, distinct() 메소드는 return 값이 없으므로 최종 파이프 메소드 중 하나인 count() 메소드를 사용했다.
Casr 4. distinct를 이용한 사람 이름이 들어 있는 배열의 중복 제거
String[] names = { "시몬", "안드레", "야고보", "요한", "빌립", "바돌로매", "도마", "마태", "야고보", "다대오", "시몬", "가룟 유다" };
Arrays.stream(names)
.distinct() // 중복값 제거
.filter(name -> name.length() == 3) // 이름이 3글자인 인물
.forEach(name -> System.out.println(name));
/*
안드레
야고보
다대오
*/
파이프의 개수가 늘어나면 배치에 따라 결과가 달라질 수 있으므로 유의한다.
Case 5. Set과 distinct를 이용한 중복 객체 제거
List<Cup> clist = new ArrayList<Cup>();
clist.add(new Cup(Cup.BLACK, 200)); // 중복
clist.add(new Cup(Cup.BLACK, 200)); // 중복
clist.add(new Cup(Cup.BLUE, 300));
clist.add(new Cup(Cup.WHITE, 400));
clist.add(new Cup(Cup.RED, 400));
clist.add(new Cup(Cup.YELLOW, 300));
clist.stream()
.distinct()
.forEach(cup -> System.out.println(cup));
System.out.println();
System.out.println();
Set과 distinct() 메소드를 이용해 중복된 객체를 제거하려면 hashCode() 재정의하고, equals() 재정의하는 방법을 이용할 수 있다.
컵 데이터 목록에서 우리가 보기에는 BLACK, 200 데이터를 가지고 있는 컵 객체가 중복된 것처럼 보이지만, 실제로는 주소값을 비교하므로 다른 컵으로 인식한다.
이를 상태가 똑같은 컵을 진짜로 같은 컵으로 인식하도록 메소드 오버라이딩을 한다.
class Cup {
public final static int BLACK = 1;
public final static int WHITE = 2;
public final static int RED = 3;
public final static int YELLOW = 4;
public final static int BLUE = 5;
private int color; // 열거값
private int size;
// 생성자
public Cup(int color, int size) {
this.color = color;
this.size = size;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
@Override
public String toString() {
return "Cup [color=" + color + ", size=" + size + "]";
}
@Override
public int hashCode() {
return ("" + this.color + this.size).hashCode();
}
@Override
public boolean equals(Object obj) {
return this.hashCode() == obj.hashCode();
}
}
개발자의 의도(중복값 제거)에 맞게 객체를 비교하기 위하여 hashCode()와 equals() 메소드 오버라이드를 했다.
열거값
"빨강, 파랑, 초록.."과 같이 데이터를 나열해 놓고, 선택하는 값들을 열거값이라고 한다.
열거값을 만들 때, 흔하게 사용하는 방법 중에 하나는 final static 변수로 만드는 것이다.
상수로 만들었기 때문에 값이 불변하며, 가독성도 높다.
이를 String 변수로 만들 수도 있지만, 문자열로 입력을 받으면 주관식 데이터를 받아야 하므로 오타가 날 가능성이 있으며, 개발자가 따로 오타 처리를 해야 한다는 불편함이 생긴다.
매핑 메소드: map(), mapToInt()
map() 메소드는 변환 작업(매핑)을 할 때 사용하며, map(), mapXXX() 형태를 가지고 있다.
쓰임새가 많아 가장 중요한 파이프 중에 하나이다.
// filter와 forEach 작업
List<String> list = Data.getStringList(10);
System.out.println(list); // [애플아케이드, 국내, 서비스, 열흘, 기존, 모바일게임, 경험, 다른, 새로운, 경험]
System.out.println();
list.stream()
.filter(word -> word.length() <= 3)
.forEach(word -> System.out.print(word + " ")); // 국내 서비스 열흘 기존 경험 다른 새로운 경험
System.out.println();
// map 작업
list.stream()
.map(word -> word.length())
.forEach(num -> System.out.print(num + " ")); // 6 2 3 2 2 5 2 2 3 2
System.out.println();
위 소스코드에서 map이 받아오는 데이터는 word.length()로 integer가 된다.
이처럼 넘겨받는 값을 가공하여 전혀 다른 값으로 변환하면서 단어가 아닌 숫자가 반환되므로, map() 메소드를 변환 메소드라고 부르기도 한다.
map을 쓰면 중간에 자유롭게 변환하여 처리하는 게 가능하다. 다만 map을 쓰면 코드가 복잡해질 가능성이 있다.
String[] names = { "시몬", "안드레", "야고보", "요한", "빌립", "바돌로매", "도마", "마태", "야고보", "다대오", "시몬", "가룟 유다" };
// 이름 추출 작업
Arrays.stream(names)
.map(name -> name.substring(1))
.forEach(name -> System.out.print(name + " ")); // 몬 드레 고보 한 립 돌로매 마 태 고보 대오 몬 룟 유다
System.out.println();
// 점수 판별 작업
List<Student> slist = new ArrayList<Student>();
slist.add(new Student("시몬", 100, 80, 90));
slist.add(new Student("안드레", 60, 50, 60));
slist.add(new Student("야고보", 70, 80, 90));
slist.add(new Student("요한", 80, 90, 40));
slist.stream()
.map(s -> {
if (s.getTotal() >= 180) {
return s.getName() + ": 합격";
} else {
return s.getName() + ": 불합격";
}
}) // Stream<Student> -> Stream<String>
.forEach(result -> System.out.println(result));
/*
시몬: 합격
안드레: 불합격
야고보: 합격
요한: 합격
*/
이름 배열에 map() 메소드를 사용해 데이터를 변환하는 작업을 진행했다.
이름 추출 작업에서는 이름의 첫 글자를 제거하고 반환하였고, 점수 판별 작업에서는 데이터를 판별하여 이름과 합격 여부를 반환하였다.
map() 메소드의 객체 반환
// Result Class 활용
List<Student> slist = new ArrayList<Student>();
slist.add(new Student("시몬", 100, 80, 90));
slist.add(new Student("안드레", 60, 50, 60));
slist.add(new Student("야고보", 70, 80, 90));
slist.add(new Student("요한", 80, 90, 40));
slist.stream()
.map(s -> {
if (s.getTotal() >= 180) {
Result r = new Result();
r.setName(s.getName());
r.setResult("합격");
return r;
} else {
Result r = new Result();
r.setName(s.getName());
r.setResult("불합격");
return r;
}
})
.forEach(r -> {
System.out.println("이름: " + r.getName());
System.out.println("결과: " + r.getResult());
});
/*
이름: 시몬
결과: 합격
이름: 안드레
결과: 불합격
이름: 야고보
결과: 합격
이름: 요한
결과: 합격
*/
// Stream<Student> -> map() -> Stream<Result>
class Result {
private String name;
private String result;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getResult() {
return result;
}
public void setResult(String result) {
this.result = result;
}
}
클래스를 활용하면 map으로 객체를 반환할 수도 있다.
map 파이프를 이용하면 이와 같이 이름과 점수 결과를 따로 분리하여 체계적으로 관리할 수 있다.
mapToInt() 메소드
int sum = Data.getIntList().stream()
.mapToInt(n -> n) // Stream<Integer>(X) IntStream(O)
.sum();
System.out.println(sum); // 4746
double avg = Data.getIntList().stream()
.mapToInt(n -> n)
.average().getAsDouble();
System.out.println(avg); // 47.46
mapToInt() 메소드는 스트림(Stream) 객체의 요소를 특정 방식으로 변환하여 새로운 IntStream을 생성하는데 사용한다.
sorted() 메소드
Data.getIntList(10)
.stream()
.sorted((a, b) -> a - b)
.forEach(n -> System.out.print(n + " ")); // 1 12 24 32 36 36 65 85 86 86
배열/컬렉션의 sort() 메소드와 사용법이 동일하며, Comparator를 구현하는 방식으로 사용한다.
💡최종 파이프 메소드
forEach() 메소드
forEach() 메소드는 향상된 for문처럼 stream으로부터 얻어낸 데이터를 1개씩 가져온다.
그리고 가져온 데이터를 Consumer.accept() 메소드의 인자로 전달하고 메소드를 호출하며, 요소만큼 반복한다.
stream.forEach(c1);
stream = list.stream();
stream.forEach(c1);
forEach() 메소드로 스트림을 뽑아내면 다시 쓸 수 없다. 이는 1회용을 의미한다.
메소드 체이닝
list.stream().forEach(str -> System.out.println(str));
이러한 작성 방식을 함수형 프로그래밍에서 메소드 체이닝이라고 한다.
집계 메소드: count(), max(), min()
List<Integer> list = Data.getIntList();
System.out.println(list.stream().count()); // 100
System.out.println(Data.getIntList().stream().count());
// Optional<Integer>
System.out.println(Data.getIntList().stream().max((a, b) -> a - b).get()); // 99
System.out.println(Data.getIntList().stream().min((a, b) -> a - b).get()); // 0
count() 메소드는 요소들을 가공해서 통계값을 낸다.
이러한 집계, 통계 메소드로는 count(), max(), min() 등이 있다.
매칭 메소드: allMatch(), anyMatch(), nonMatch()
int[] nums = { 1, 2, 3, 4, 5 };
boolean result = false;
for (int n : nums) {
if (n % 2 == 1) {
result = true;
break;
}
}
if (result) {
System.out.println("홀수 발견");
} else {
System.out.println("짝수 배열입니다.");
}
// match() 메소드
System.out.println(Arrays.stream(nums).allMatch(n -> n % 2 == 0)); // false
System.out.println(Arrays.stream(nums).anyMatch(n -> n % 2 == 0)); // true
System.out.println(Arrays.stream(nums).noneMatch(n -> n % 2 == 0)); // false
매칭 메소드는 스트림 요소가 제시하는 조건을 만족하는지, 만족하지 않는지를 판단한다.
최종 파이프이므로 stream을 돌려주지 않으며, 대신에 boolean 값을 돌려준다.
매칭 메소드 형식
- boolean allMatch(Predicate): 모든 요소가 조건을 100% 만족하면 true를 반환한다.
- boolean anyMatch(Predicate): 일부 요소가 조건을 만족하면 true를 반환한다.
- boolean noneMatch(Predicate): 모든 요소가 조건을 만족하지 않을 때 true를 반환한다.