@FunctionalInterface
interface calculator {
void operate(int a, int b)
}
Calculator add = new Calculator() {
@Override
public int operate(int a, int b) {
return a+b;
}
};
Calculator multiply = new Calculator() {
@Override
public int operate(int a, int b) {
return a*b;
}
};
이 인터페이스를 활용해서 연산자를 만들려면 원래는 익명함수를 사용해서 했다.
너무 복잡하다.. 🤮
@FunctionalInterface
interface calculator {
void operate(int a, int b)
}
// @FunctionalInterface가 있는 인터페이스만 람다식을 사용할 수 있다.
// 그리고 이 어노테이션은 장식이고, 실제로는 함수 하나만 있으면 된다.
Calculator add = (a, b) -> a + b;
Calculator multiply = (a, b) -> a * b;
이런 방법이 너무 복잡해서, 우리는 람다식으로 조금 편리하게 나타내보기로 했다.
이렇게 하니, 이전 코드보다 훨씬 깔끔해졌고, 가독성도 더욱 높아졌다. 😀
List<String> names1 = Arrays.asList("John", "Jane", "Tom", "Jerry", "Alice");
names1.add("Dongsuck"); // UnsupportedOperationException 발생
Arrays.asList() 라는 식으로 선언하고, 정적할당하는 방식이다.
그래서, 아래 코드처럼 리스트에 하나를 추가하면 예외가 발생한다.
List<String> names2 = new ArrayList<>();
names2.add("엄준식");
names2.add("하이");
new ArrayList<>(); 라는 식으로 선언하고, 동적할당하는 방식이다.
그래서, 아래 코드처럼 여러 문자열을 리스트에 추가할 수 있다.
정해진 것은 없지만, 값이 상수 더 이상 바뀌지 않을 때에는, 정적할당 방식, 값이 계속 바뀌고 넣고 빼지고 할 때에는, 동적할당 방식으로 선언하는 것이 바람직하다.
데이터를 처리하는 강력한 도구
코드의 가독성을 높이고, 복잡한 작업을 간결하게 처리할 수 있습니다. 기본적인 필터링, 변환, 정렬뿐만 아니라, 병렬 처리와 데이터 축소 등의 기능을 쉽게 사용할 수 있음
Stream 메서드에는 여러가지 있지만, 우리는 그것 중 MAP, FILTER, REDUCE 를 배워보려고 한다.
// Example
List<String> filteredNames = names1.stream()
.filter(name -> name.startsWith("J"))
.collect(Collectors.toList());
위처럼, 코드를 짜면, names1에 있는 문자열중, “J”로 시작하는 애들을 리스트로 만들고 그 리스트를
filteredNames에 저장한다는 뜻이다.
Q: 람다식을 이용하여서 숫자 리스트에서 짝수만 필터링하는 코드를 작성하시오
// Practice
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
List<Integer> filteredNumber = numbers.stream()
.filter(num -> num % 2 == 0)
.collect(Collectors.toList());
이렇게 코드를 짜면, numbers에 있는 리스트가 계속 반복이 돌고,
짝수인 애들만 필터링을 해서, List로 만들고 그 List를 filteredNumber에 넣어준다.
// Example
List<String> names = Arrays.asList("John", "Jane", "Jack", "Jill", "Bob");
List<String> upperCaseNames = names.stream()
// String 클래스에서 제공하는 메서드 이기 때문에 그냥 String::toUpperCase 라는 식으로 적으면 된다.
.map(String::toUpperCase)
.collect(Collectors.toList());
⛔️ 여기서 잠깐!!!!
왜 람다식을 쓰지 않고, String::toUpperCase를 사용했을까?
이유는, 코드가 간결해지고, 가독성이 높아지기 때문이다.
위처럼, 코드를 짜면, names에 있는 리스트들을 모두 반복을 돌아서, 대문자로 바뀌게 된다.
그 다음 바뀐 결과를 upperCaseNames에 넣어준다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> squaredNumbers = numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(squaredNumbers);
위 코드는, 1, 2, 3, 4, 5가 담겨진 리스트를 반복하면서 각 요소들을 제곱해서 다시 리스트로 만들어서 squaredNumbers에 넣는 과정이다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// reduce() 는 리스트의 모든 값을 하나로 합칠 때 사용
// identity 는 초기값을 의미
// 여기서 identity는 1
Integer reduce = numbers.stream().reduce(1, (a, b) -> a * b);
System.out.println(reduce);
-> 120
이 코드는, 1부터 1, 2, 3, 4, 5를 모두 곱해서, reduce 변수에 저장함
List<String> strings = Arrays.asList("asn", "sad", "fsdjnif");
Integer reduce = strings.stream()
.map(String::length)
.reduce(0, Integer::sum);
System.out.println(reduce);
-> 13
이 코드는, 0부터 strings안에 있는 문자열의 길이를 합해서, reduce 변수에 저장함
Null인 상태에 객체의 메서드에 접근하려 할 때, 발생하는 예외
String value = null;
value.length() // NPE 발생
Optional<String> value = getFromServer2();
int length = value.map(String::length).orElse(0);
System.out.println(length);
이렇게 NULL이 될 수 있는 값을 Optional로 주게 된다면, NULL일 때, NULL이 아닐 때, 두 개의 분기로 나눠서 처리할 수 있게 된다.
// <?>를 사용하면 모든 타입의 리스트를 받을 수 있음. (와일드 카드)
public static void printList(List<?> list) {
for(Object o : list) {
System.out.println(o);
}
}
public static void main(String[] args) {
List<String> strList = Arrays.asList("a", "b", "c");
List<Integer> intList = Arrays.asList(1, 2, 3);
printList(strList);
printList(intList);
}
위 코드처럼, 자료형에 상관없이 모든 자료형을 다 받을 수 있는 자료형
이렇게 짜면, 리스트에 있는 각 요소에 toString()이 출력되고 줄바꿈이 된다.
public static double getAverage(List<? extends Number> numbers) {
double sum = 0.0;
for (Number number : numbers) {
sum += number.doubleValue();
}
return sum / numbers.size();
}
public static void main(String[] args) {
List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5);
List<Double> doubleList = Arrays.asList(1.5, 2.5, 3.5, 4.5);
System.out.println("정수 리스트 평균: " + getAverage(intList));
System.out.println("실수 리스트 평균: " + getAverage(doubleList));
}
위 코드는, Number를 상속받은 자료형에만 해당한다.
또한, int, double은 되지 않는다.
int랑 double은 primitive type이기 때문이다. (data type)
Integer랑 Double은 wrapper class이고, Number를 상속받았기 때문에, 저 조건에 부합된다.
동기에 반대되는 개념이다.
동기는 하나의 명령어가 다 처리된 후, 다음 명령어로 가는 것인 반면,
비동기는 빨리 처리되는 명령어를 먼저 처리하는 것이다.
CompletableFuture.supplyAsync(() -> {
try {
// 오래걸리는 스레드
Thread.sleep(2000);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 끝나면 리턴
return "비동기 작업이 오랜시간 끝에 드디어 완료!";
}).thenAccept(System.out::println); // 리턴값을 출력
System.out.println("메인 스레드 실행 중....");
// Main Thread 가 죽으면 저기 있는 Thread 도 죽어서 Main Thread 를 늦게 끝내야 함.
try {
Thread.sleep(3000);
}
catch (InterruptedException e) {
throw new RuntimeException(e);
}
이 코드를 실행시키면, 메인 스레드 실행 중…이 뜬 후, 비동기 작업이 오랜시간 끝에 드디어 완료! 라고 뜨게 된다.
?안에는 비동기 후, 결과값의 타입을 넣어주면 된다.
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() ->
10);
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() ->
20);
f1.thenCombine(f2, Integer::sum)
.thenAccept(System.out::println);
CompletableFuture.supplyAsync 이것을 사용하면, 비동기 처리를 할 수 있는 구문이 된다.
f1.thenCombine(f2, Integer::sum)
.thenAccept(System.out::println);
이 구문은 f1이 다 되고, f2랑 sum이 다 된다면, 출력하라는 의미이다.