프로그래밍 언어에서 특정 유형의 값이나 객체가 다른 값들과 동등하게 취급될 수 있다는 개념
사용할 때, 다른 요소들과 차별점이 없이 동등하다!
일급 객체는 변수에 저장할 수 있다.
const x = 42; // 숫자 42를 변수 x에 할당
const sayHello = function() { console.log("Hello!"); }; // 함수를 변수에 할당
일급 객체는 배열이나 객체 등의 데이터 구조에 저장할 수 있다.
const array = [1, 2, sayHello]; // 함수가 배열에 저장됨
const obj = { number: 42, greet: sayHello }; // 함수가 객체의 프로퍼티로 저장됨
일급 객체는 함수의 인자로 사용할 수 있다.
function execute(func) {
func();
}
execute(sayHello); // 함수가 다른 함수의 인자로 전달됨
일급 객체는 함수의 반환값으로 사용 가능하다.
function createGreeter() {
return function() { console.log("Hello there!"); };
}
const greeter = createGreeter(); // 함수가 반환됨
greeter(); // "Hello there!" 출력
일급 객체는 런타임에 동적으로 프로퍼티를 추가하거나 변경할 수 있다.
sayHello.language = "English";
console.log(sayHello.language); // "English" 출력
자바스크립트에서의 함수는 일급 객체의 모든 특성을 갖고 있다. (즉, 함수가 일급객체로서의 특징을 가지므로 일급 함수가 된다! 일급 함수에 관한건 밑에서...)
// 1. 변수에 할당
const greet = function() {
console.log("Hello, world!");
};
// 2. 데이터 구조에 저장
const functions = [greet, function() { console.log("Hi!"); }];
// 3. 함수의 인자로 전달
function callFunction(func) {
func();
}
callFunction(greet); // "Hello, world!" 출력
// 4. 함수의 반환값으로 사용
function createFunction() {
return function() {
console.log("This is a returned function.");
};
}
const newFunction = createFunction();
newFunction(); // "This is a returned function." 출력
// 5. 동적으로 프로퍼티 추가
greet.language = "English";
console.log(greet.language); // "English" 출력
일급 함수(First-Class Function)는 프로그래밍 언어에서 함수를 일급 객체로 다룬다는 것을 의미한다. 즉 함수가 다른 데이터 타입과 마찬가지로 변수에 할당되거나 데이터 구조에 저장될 수 있으며, 함수의 인자나 반환값으로 사용될 수 있다는 개념이다.
일급 함수의 개념은 함수형 프로그래밍을 가능하게 하고, 코드의 유연성과 재사용성을 크게 향상시킨다.
함수가 일급객체의 특성을 갖는다는 것을 의미하므로, 즉, 함수가 다른 데이터 타입(예: 숫자, 문자열, 객체)과 동일하게 취급된다는 것을 의미한다. 이를통해 함수가 매우 유연하고 강력한 프로그래밍 도구로 사용 될 수 있다.
일급 함수의 개념을 활용하면, 고차 함수, 콜백 함수, 클로저와 같은 강력한 프로그래밍 패턴을 구현할 수 있다. 이러한 패턴들은 코드의 유연성을 높이고, 재사용성을 증대시키며, 유지보수성을 향상시킨다.
고차함수는 다른 함수를 인자로 받거나 함수를 반환하는 함수이다.
function map(array, func) {
const result = [];
for (let i = 0; i < array.length; i++) {
result.push(func(array[i]));
}
return result;
}
const numbers = [1, 2, 3];
const doubled = map(numbers, function(x) {
return x * 2;
});
console.log(doubled); // [2, 4, 6]
콜백 함수는 다른 함수의 인자로 전달되어 특정 작업이 완료된 후 호출되는 함수이다
function fetchData(callback) {
setTimeout(function() {
const data = "Some data";
callback(data);
}, 1000);
}
fetchData(function(data) {
console.log(data); // "Some data" 출력 (1초 후)
});
클로저는 함수가 선언될 때의 환경(lexical scope)을 기억하여, 그 함수가 다른 환경에서 호출되더라도 그 환경에 접근할 수 있게 한다.
function makeCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
자바에서는 기본적으로 함수(메소드)가 일급 객체는 아니지만, 자바 8 버전에서 도입된 람다 표현식(lambda expressions)을 통해 함수를 일급 객체처럼 다룰 수 있게 되었다. 람다 표현식은 함수형 프로그래밍의 특징을 도입하여, 메소드를 하나의 식(expression)으로 표현할 수 있게 해준다.
이는 메소드를 일반 객체처럼 변수에 할당하거나 다른 메소드의 인자로 전달할 수 있게 해주는 기능을 제공한다.
자바에서 람다 표현식은 주로 함수형 인터페이스(functional interface)를 통해 사용된다. 함수형 인터페이스는 하나의 추상 메소드만을 가진 인터페이스로. 람다 표현식은 이 추상 메소드의 구현체로 사용된다.
람다 표현식은 익명 함수(anonymous function)를 간결하게 표현하는 방법으로, 이를 통해 자바에서도 함수를 변수에 할당하거나, 함수의 인자로 전달하거나, 함수의 반환값으로 사용할 수 있게 되었다.
람다 표현식을 사용하려면 함수형 인터페이스가 필요하다. 함수형 인터페이스는 하나의 추상 메소드를 가지는 인터페이스이다.
@FunctionalInterface 어노테이션을 사용하여 함수형 인터페이스임을 명시할 수 있다.
@FunctionalInterface
interface MyFunction {
void apply();
}
(parameters) -> expression
(parameters) -> { statements; }
MyFunction myFunction = () -> System.out.println("Hello, world!");
myFunction.apply(); // "Hello, world!" 출력
@FunctionalInterface
interface MyFunctionWithParam {
void apply(int x);
}
MyFunctionWithParam myFunctionWithParam = (x) -> System.out.println("Value: " + x);
myFunctionWithParam.apply(5); // "Value: 5" 출력
@FunctionalInterface
interface MyFunctionWithParam {
void apply(int x);
}
MyFunctionWithParam myFunctionWithParam = (x) -> System.out.println("Value: " + x);
myFunctionWithParam.apply(5); // "Value: 5" 출력
자바의 표준 라이브러리에는 이미 많은 함수형 인터페이스가 포함되어 있다. 가장 대표적인 예로 java.util.function패키지 내의 인터페이스들이 있다.
Predicate인터페이스는 주어진 인자를 테스트하여 boolean값을 반환하는 함수형 인터페이스이다. 이를 통해 필터링 작업을 간편하게 수행할 수 있다.
import java.util.function.Predicate;
public class LambdaExample {
public static void main(String[] args) {
Predicate<Integer> isEven = (x) -> x % 2 == 0;
System.out.println(isEven.test(4)); // true
System.out.println(isEven.test(3)); // false
}
}
Function인터페이스는 하나의 인자를 받아 다른 타입의 값을 반환하는 함수형 인터페이스이다.
import java.util.function.Function;
public class LambdaExample {
public static void main(String[] args) {
Function<String, Integer> stringLength = (s) -> s.length();
System.out.println(stringLength.apply("Hello")); // 5
System.out.println(stringLength.apply("Lambda")); // 6
}
}
Supplier인터페이스는 인자를 받지 않고 값을 반환하는 함수형 인터페이스이다. 주로 데이터를 공급하는 역할을 한다.
import java.util.function.Supplier;
public class SupplierExample {
public static void main(String[] args) {
// Supplier를 사용하여 문자열을 반환하는 람다 표현식
Supplier<String> supplier = () -> "Hello, Supplier!";
// Supplier를 호출하여 값을 얻음
String result = supplier.get();
System.out.println(result); // "Hello, Supplier!" 출력
}
}
Consumer인터페이스는 인자를 받아 어떤 동작을 수행하고 값을 반환하지 않는 함수형 인터페이스이다. 주로 입력값에 대한 어떠한 처리를 수행할 때 사용된다.
import java.util.function.Consumer;
public class ConsumerExample {
public static void main(String[] args) {
// Consumer를 사용하여 문자열을 출력하는 람다 표현식
Consumer<String> consumer = (s) -> System.out.println(s);
// Consumer를 호출하여 문자열을 출력
consumer.accept("Hello, Consumer!"); // "Hello, Consumer!" 출력
}
}
BiFunction인터페이스는 두 인자를 받아 값을 반환하는 함수형 인터페이스이다. 두 개의 입력값을 반아서 결과값을 생성하는 경우에 사용된다.
import java.util.function.BiFunction;
public class BiFunctionExample {
public static void main(String[] args) {
// BiFunction을 사용하여 두 정수를 더하는 람다 표현식
BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;
// BiFunction을 호출하여 두 정수의 합을 구함
int result = add.apply(3, 5);
System.out.println(result); // 8 출력
}
}
import java.util.function.Predicate;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.Consumer;
import java.util.function.BiFunction;
public class LambdaCombinedExample {
public static void main(String[] args) {
// Predicate 예시: 문자열의 길이가 5 이상인지 검사
Predicate<String> isLongerThanFive = (s) -> s.length() >= 5;
System.out.println(isLongerThanFive.test("Hello")); // true
System.out.println(isLongerThanFive.test("Hi")); // false
// Function 예시: 문자열을 정수로 변환
Function<String, Integer> stringToLength = (s) -> s.length();
System.out.println(stringToLength.apply("Hello")); // 5
// Supplier 예시: 고정된 문자열을 반환
Supplier<String> stringSupplier = () -> "Generated String";
System.out.println(stringSupplier.get()); // "Generated String"
// Consumer 예시: 문자열을 출력
Consumer<String> stringConsumer = (s) -> System.out.println("Consumed: " + s);
stringConsumer.accept("Test String"); // "Consumed: Test String"
// BiFunction 예시: 두 정수를 더한 후 결과를 문자열로 변환
BiFunction<Integer, Integer, String> addAndConvert = (x, y) -> "Result: " + (x + y);
System.out.println(addAndConvert.apply(4, 7)); // "Result: 11"
// 모든 기능을 합친 예시
String input = "Example";
// Predicate를 사용하여 문자열의 길이가 5 이상인지 검사
if (isLongerThanFive.test(input)) {
// Function을 사용하여 문자열의 길이를 구함
int length = stringToLength.apply(input);
// BiFunction을 사용하여 길이에 10을 더하고 결과를 문자열로 변환
String result = addAndConvert.apply(length, 10);
// Consumer를 사용하여 결과를 출력
stringConsumer.accept(result); // "Consumed: Result: 17"
}
// Supplier를 사용하여 문자열을 생성하고 Consumer로 출력
stringConsumer.accept(stringSupplier.get()); // "Consumed: Generated String"
}
}
람다 표현식은 스트림 API와 함께 사용될 때 매우 강력하다. 스트림 API는 컬렉션 데이터를 처리하는 다양한 방법을 제공한다. 예를 들어, 필터링, 매핑, 집계 등의 작업을 람다 표현식과 함께 간결하게 구현할 수 있다.
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.Consumer;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
public class LambdaStreamExample {
public static void main(String[] args) {
// Sample data
List<String> names = Arrays.asList("John", "Jane", "Jack", "Doe", "Jill", "Bob");
// Predicate: 이름의 길이가 3 이상인지 검사
Predicate<String> lengthGreaterThanThree = (s) -> s.length() > 3;
// Function: 이름을 대문자로 변환
Function<String, String> toUpperCase = String::toUpperCase;
// Supplier: 기본 메시지 제공
Supplier<String> defaultMessage = () -> "No names found";
// Consumer: 이름을 출력
Consumer<String> printName = (s) -> System.out.println("Name: " + s);
// BiFunction: 두 문자열을 합쳐서 반환
BiFunction<String, String, String> concatenate = (s1, s2) -> s1 + " & " + s2;
// 스트림을 사용하여 필터링, 변환, 출력 수행
List<String> result = names.stream()
.filter(lengthGreaterThanThree) // 길이가 3 초과인 이름 필터링
.map(toUpperCase) // 이름을 대문자로 변환
.collect(Collectors.toList()); // 결과를 리스트로 수집
// 결과가 비어있으면 기본 메시지를 출력하고, 그렇지 않으면 이름들을 출력
if (result.isEmpty()) {
System.out.println(defaultMessage.get());
} else {
result.forEach(printName);
}
// BiFunction을 사용하여 리스트의 첫 두 이름을 결합하여 출력
if (result.size() >= 2) {
String combined = concatenate.apply(result.get(0), result.get(1));
System.out.println("Combined: " + combined);
}
}
}
// Name: JOHN
// Name: JANE
// Name: JACK
// Name: JILL
// Combined: JOHN & JANE
names 리스트의 각 요소를 처리한다.filter 메소드를 사용하여 Predicate를 적용하여 길이가 3 초과인 이름들만 남긴다.map 메소드를 사용하여 Function을 적용하여 이름을 대문자로 변환한다.위와 같이 스트림 API와 함수형 인터페이스를 결합하여 데이터를 처리하면 코드의 가독성과 유지보수성을 높일 수 있다.