람다

OneTwoThree·2023년 7월 19일
0

자바

목록 보기
10/19

출처 : 윤성우의 열혈 Java 프로그래밍


Nested Class와 Inner 클래스

public class Main {
    
    class Inner{
        
    }
    
    static class Nested{
        
    }
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

클래스 내의 클래스를 nested class라 한다.
nested 클래스 중 static 선언이 되지 않은 클래스를 inner 클래스라 한다.

이너 클래스는 세가지로 나뉜다

  • 멤버 이너 클래스
  • 로컬 이너 클래스
  • 익명 이너 클래스
    보통 이너를 생략하고
  • 멤버 클래스
  • 로컬 클래스
  • 익명 클래스
    라고 부른다.

static nested class

static 선언이 갖는 특성이 반영된 클래스이다

class Outer{
    
    private static int num = 0;    
    static class Nested{
        public void add(int n){
            num+=n;
        }
    }
}

public class Main { 
    public static void main(String[] args) {
        Outer.Nested n = new Outer.Nested();
        n.add(1);
    }
}

위와 같이 외부 클래스 이름을 포함한 형태로 객체를 생성한다.
그리고 static nested class의 인스턴스 생성은 외부 클래스 인스턴스를 생성하지 않고도 생성할 수 있다.
때문에 static nested class에서는 외부 클래스의 static으로 선언된 변수와 메소드에만 접근이 가능하다.

Inner class

네스티드 클래스 중 static 선언이 붙지 않은 것이 inner class이다.
이너 클래스는 멤버 클래스, 로컬 클래스, 익명 클래스로 나뉜다.
멤버 클래스는 인스턴스 변수, 인스턴스 메소드와 동일한 위치에 정의된 클래스이다.
로컬 클래스는 중괄호 내(특히 메소드 내)에 정의된 클래스이다.

class Outer{
    class MemberClass{

    }

    void method(){
        class LocalClass{
            
        }
    }
}

예시는 위와 같다.


interface Printable{
    void print();
}

class Papers{
    private String con;
    public Papers(String s) {con=s;}

    public Printable getPrinter(){
        return new Printable(){
            public void print(){
                System.out.println(con);
            }
        };
    }

}

public class Main {
    public static void main(String[] args) {
        Papers p = new Papers("서류");
        Printable prn = p.getPrinter();
        prn.print();
    }
}

익명 클래스는 위 예시를 통해 이해할 수 있다.
getPrinter()메소드 내부를 보면 Printable은 인터페이스인데, 인터페이스를 생성하면서 구현하는 클래스의 정의를 덧붙여서 인스턴스 생성이 가능하게 했다.

람다

람다를 사용하면 코드를 줄일 수 있고 가독성도 뛰어나다
익명 클래스 예시에서 사용한 코드를 람다로 줄일 수 있다.

interface Printable{ //추상 메소드가 하나인 인터페이스
    void print(String s);
}
public class Main {
    public static void main(String[] args) {
        Printable prn = (s)->System.out.println(s); //람다식
        prn.print("서류");
    }
}

(s)->System.out.println(s) 부분이 람다식이다.
람다와 익명 클래스는 다르다. 하지만 둘 다 인스턴스의 생성으로 이어진다.
핵심은 Printable이 추상 메소드가 하나뿐인 인터페이스라는 것이다.


람다식의 인자 전달

람다식을 메소드의 인자로 전달할 수 있다.
method((s)->System.out.println(s));

예시는 아래와 같다

interface Printable{ //추상 메소드가 하나인 인터페이스
    void print(String s);
}
public class Main {
    public static void ShowString(Printable p, String s){
        p.print(s);
    }
    public static void main(String[] args) {
        ShowString((s)->{System.out.println(s);},"서류");
    }
}

예시에서 ShowString(Printable p, String s) 의 첫번째 인자로 람다식을 전달해서 첫번째 매개변수인 Printable p를 초기화한다
Printable p = (s)->{System.out.println(s);} 로 초기화 한 것과 같다.

람다와 함수형 인터페이스

람다는 인스턴스보다 기능 하나가 필요한 상황을 위해 사용한다.
프로그램을 작성하다 보면 기능 하나를 정의해서 전달해야 하는 상황이 있다.
예를 들어 Comparator<T> 인터페이스의 구현이 필요한 상황이다.

import java.util.*;

class SLencomp implements  Comparator<String>{

    @Override
    public int compare(String s1, String s2) {
        return s1.length()-s2.length(); //짧은게 앞에 오도록 
    }
}

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Robot");
        list.add("Lambda");
        list.add("Box");

        Collections.sort(list, new SLencomp()); // 정렬

        for (String s: list){
            System.out.println(s);
        }

    }
}

위 코드를 보면 Collections.sort()를 호출하면서 두번째 인자로 정렬의 기준을 갖는 SLenComp 객체를 생성해서 전달한다.
객체를 전달하지만, 필요한 것은 객체 내에 있는 메소드 기능을 전달하는 것이다.

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("Robot");
        list.add("Lambda");
        list.add("Box");

        Collections.sort(list, (s1,s2)->s1.length()-s2.length()); // 정렬

        for (String s: list){
            System.out.println(s);
        }

    }
}

이런 식으로 람다식을 사용해서 기능을 전달하도록 변경할 수 있다.

매개변수가 있고 반환하지 않는 람다식

interface Printable{
    void print(String s); //매개변수 하나, 반환형 void인 메소드
}

public class Main {
    public static void main(String[] args) {
        Printable p;
        //줄임 없는 표현
        p = (String s)->{System.out.println(s);};
        //중괄호 생략
        p = (String s)-> System.out.println(s);
        //매개변수 형 생략
        p = (s)->System.out.println(s);
        //매개변수 소괄호 생략
        p = s->System.out.println(s);
    }
}
  • 메소드 몸체의 중괄호 생략은 메소드 몸체가 하나의 문장일 경우 가능하다.
  • 중괄호를 생략할 때는 문장 끝의 ;도 같이 지운다.
  • 매개변수가 String 형인 것은 컴파일러 입장에서 유추가 가능하므로 생략이 가능하다
  • 매개변수가 하나일 경우 매개변수의 소괄호도 생략 가능하다
  • 메소드 몸체가 둘 이상의 문장이거나 매개변수 수가 둘 이상일 경우에는 각각 중괄호, 소괄호 생략이 불가능하다.

매개변수가 둘이고 반환하지 않는 람다식

interface Calculate{
    void cal(int a, int b);
}

public class Main {
    public static void main(String[] args) {
        Calculate c;
        c = (a,b)->System.out.println(a+b);
        c = (a,b)->System.out.println(a-b);
    }
}

형태는 다음과 같다.
마찬가지로 몸체 문장이 1개여서 중괄호를 생략했다

매개변수가 있고 반환하는 람다식

interface Calculate{
    int cal(int a, int b);
}

public class Main {
    public static void main(String[] args) {
        Calculate c;
        c = (a,b)-> {return a+b;};
        c = (a,b) -> a+b;
    }
}

첫번째 람다식의 경우 메소드 몸체에 해당하는 내용이 return문이면 그 문장이 하나더라도 중괄호의 생략이 불가능하다.
두번째 람다식은 메소드 몸체에 연산이 등장하는데 이 연산의 결과로 값이 남게 되고 그럼 별도로 명시하지 않아도 반환의 대상이 된다.
따라서 return문이 메소드 몸체를 이루는 유일한 문장이면 이렇게 작성할 수 있고, 이게 보편적인 방식이다.

매개변수가 없는 람다식

import java.util.Random;

interface Generator{
    int rand();
}

public class Main {
    public static void main(String[] args) {
        Generator gen = ()->{
            Random rand = new Random();
            return rand.nextInt();
        };
    }
}

매개변수가 없는 람다식은 매개변수를 표현하는 소괄호 안을 비우면 된다.
그리고 참고할 점은 메소드 몸체가 둘 이상의 문장이므로 반드시 중괄호로 감싸주어야 하고 return으로 반환해야 한다.

함수형 인터페이스와 애노테이션

지금까지 확인한 예제들을 보면 인터페이스에 추상 메소드가 딱 하나만 선언되어 있다. 이러한 인터페이스들을 함수형 인터페이스라 하고 람다식은 함수형 인터페이스를 기반으로만 작성될 수 있다.

  • @FunctionalInterface 애노테이션으로 함수형 인터페이스가 맞는지 확인할 수 있다.
  • static이나 default 선언이 붙은 메소드는 함수형 인터페이스의 정의에 영향을 미치지 않는다.
@FunctionalInterface
interface Calculate<T>{ //제네릭 기반의 함수형 인터페이스 
    T cal(T a, T b);
}
public class Main {
    public static void main(String[] args) {
        Calculate<Integer> ci = (a,b)->a+b;
        Calculate<Double> cd = (a,b)->a+b; 
    }
}

다음과 같이 제네릭 기반으로 정의된 함수형 인터페이스에도 람다식을 사용할 수 있다.

@FunctionalInterface
interface Calculate<T>{ //제네릭 기반의 함수형 인터페이스
    T cal(T a, T b);
}
public class Main {
    public static <T> void calAndShow(Calculate<T> op, T n1, T n2){
        T r = op.cal(n1,n2);
        System.out.println(r);
    }
    public static void main(String[] args) {
        calAndShow((a, b)->a+b,3,4);
        calAndShow((a,b)->a+b, 2.5,7.1);
        calAndShow((a,b)->a-b,4,2);
        calAndShow((a,b)->a-b,4.9,3.2);


    }
}

이런 식으로 제네릭 타입이 유추가 가능한 상황에서는 위와 같이 람다식을 작성할 수 있다.

1개의 댓글

comment-user-thumbnail
2023년 7월 19일

정말 깊이 있는 글이었습니다.

답글 달기