코딩하는 털보

STUDY HALLE - 15주차 본문

Diary/Study Halle

STUDY HALLE - 15주차

이정인 2021. 3. 5. 16:28

목표

자바의 람다식에 대해 학습하세요.

학습할 것

  • 익명 클래스
    • 로컬 클래스
    • Variable Capture
    • 익명 클래스
  • 람다식 사용법
    • 람다식
    • 람다식 사용법
  • 함수형 인터페이스
  • 메소드, 생성자 레퍼런스

익명 클래스

로컬 클래스

메소드 구현부에서 정의되는 클래스, 모든 메소드 블록 내에 로컬 클래스를 정의 할 수 있다.

Cafe 클래스의 addMusic 메소드 구현부에 있는 Music 클래스

public class Cafe {

    List<String> musicList = new ArrayList<>();

    public List<String> getMusicList() {
        return musicList;
    }

    public void addMusicOnList(String name, String singer) {

        int listNumber = 1;

        class Music {
            public String name;
            public String singer;

            public Music(String name, String singer) {
                if ( search(name, singer) ) {
                    this.name = name;
                    this.singer = singer;
                }
            }

            public boolean search(String name, String singer) {
                //search module..
                return true;
            }

            public void addMusic() {
                //listNumber = 9;
                musicList.add(this.name);
                System.out.println(listNumber + "번 리스트에 음악을 추가합니다.");
            }
        }

        Music music = new Music(name, singer);
        music.addMusic();
    }

    public static void main(String[] args) {
        Cafe cafe = new Cafe();
        cafe.addMusic("The Only Exception","paramore");

        System.out.println(cafe.getMusicList().get(0));
    }
}

Music 클래스의 addMusic 메소드

            public void addMusic() {
                musicList.add(this.name);
            }

보이는 것 처럼 로컬 클래스에서는 이 로컬 클래스를 포함하는 클래스의 멤버(musicList)에 접근할 수 있다.

addMusicOnList 메소드의 멤버변수 listNumber

    public void addMusicOnList(String name, String singer) {

        int listNumber = 1;

        class Music {

                        ...

            public void addMusic() {
                //listNumber = 9; //컴파일 에러, 멤버변수 변경 불가능.
                musicList.add(this.name);
                System.out.println(listNumber + "번 리스트에 음악을 추가합니다.");
            }
        }

        Music music = new Music(name, singer);
        music.addMusic();
    }

보이는 것과 같이 멤버변수를 참조할 수도 있다. 이 부분은 자바 버전에 따라 차이점이 있는데, Java 8 이전 버전에서는 멤버변수가 final이어야 접근이 가능하지만 Java 8 부터는 final이 아니어도 접근할 수 있다. 그러나 final인것 처럼 로컬 클래스에서 멤버변수의 변경 작업은 할 수 없다.

static 메소드의 로컬 클래스

public class Cafe {

    //musicList 필드를 static으로 변경함.
    static List<String> musicList = new ArrayList<>();

    public List<String> getMusicList() {
        return musicList;
    }

    //addMusic이 static 인 것에 집중.
    public static void addMusic(String name, String singer) {

        int listNumber = 1;

        class Music {

            ...

            public void addMusic() {
                //listNumber = 9;
                musicList.add(this.name); //musicList가 static이 아니면 접근할 수 없다!
                System.out.println(listNumber + "번 리스트에 음악을 추가합니다.");
            }
        }

        Music music = new Music(name, singer);
        music.addMusic();
    }

    public static void main(String[] args) {
        Cafe.addMusic("The Only Exception","paramore");

        System.out.println(Cafe.musicList.get(0));
    }
}

정적 메소드안에있는 로컬클래스는 정적 멤버에만 접근할 수 있다.

로컬 클래스는 static일 수 없으며, 클래스가 아닌 interface를 만들 수도 없다. (interface는 본질적으로 static 이므로...) 또한 정적 멤버를 로컬 클래스에서 정의할 수 없다.

public void addMusic(String name, String singer) {

        int listNumber = 1;

//        interface Music { // 컴파일 에러
//            public void addMusic();
//        }

//        static class CafeMusic { // 컴파일 에러
        class CafeMusic {
            //public static String name; // 컴파일 에러

            ...

            //public static boolean search(String name, String singer) { // 컴파일 에러
            public boolean search(String name, String singer) {
                //search module..
                return true;
            }
      ...
}

하지만 프리미티브 타입 또는 String 타입이 컴파일 시점에 상수로 판단되는 경우에 한하여(final, 초기화 되어있는) static 가능.

package javalambda;

import java.util.ArrayList;
import java.util.List;

public class Cafe {

    List<String> musicList = new ArrayList<>();

    public List<String> getMusicList() {
        return musicList;
    }

    public void addMusic() {

        int listNumber = 1;

        class CafeMusic {

            //final, 초기화로 상수이므로 static 가능.
            public static final String name = "test";
            public static final String singer = "test";

            public void addMusic() {
                musicList.add(name);
                System.out.println(listNumber + "번 리스트에 음악을 추가합니다.");
            }
        }

        CafeMusic music = new CafeMusic();
        music.addMusic();
    }

    public static void main(String[] args) {
        Cafe cafe = new Cafe();
        cafe.addMusic();

        System.out.println(cafe.getMusicList().get(0));
    }
}

Variable Capture

위에서 알아봤듯이 로컬 클래스는 final로 선언 된 지역 변수에만 액세스 할 수 있었다. 로컬 클래스가 해당 로컬 클래스를 덮고 있는 주변 블록의 지역 변수 또는 매개 변수에 접근하는 것을 해당 변수 또는 매개 변수를 캡쳐 한다고 표현한다.

캡쳐라는 표현은 말로만 그런것이 아니라 실제로도 변수를 copy하는 것과 같이 동작한다. JVM에서 지역 변수는 쓰레드 마다의 스택 영역에 저장되는데, 로컬 클래스는 인스턴스화 되는 시점에서 이 지역 변수를 참조하여 자신의 쓰레드의 스택 영역에 저장하게 된다. 그렇기 때문에 접근하는 지역 변수를 복사한 이 후로 변하지 않도록 final과 같은 제한이 생긴것이다.

Java SE 8부터 로컬 클래스는 final 또는 사실상 final 인 지역 변수 및 매개 변수를 캡쳐 할 수 있다. 초기화 후 값이 변경되지 않는 변수 또는 매개 변수를 사실상 final이라고 한다.

    public void addMusicOnList(String name, String singer) {

        final int musicNumber = 1;
        //listNumber는 로컬 클래스에서 변경되지 않는다면, '사실상 final' 이다.
        int listNumber = 1;

        class Music {
            public String name;
            public String singer;

            public Music(String name, String singer) {
                this.name = name;
                this.singer = singer;
            }

            public void addMusic() {
                //listNumber = 9; //final이 아님에도 불구하고 변경하면 안된다.
                musicList.add(this.name);
                System.out.println(listNumber + "번 리스트에 "+musicNumber+"음악을 추가합니다.");
            }
        }

        Music music = new Music(name, singer);
        music.addMusic();
    }

익명 클래스

이름이 없는 로컬 클래스이다. 만약 로컬 클래스를 한번만 사용하는 경우 익명 클래스를 사용하여 코드를 간결하게 할 수 있다. 익명 클래스는 로컬 클래스와 다르게 어떤 클래스의 선언이 아닌 표현식을 통해 선언한다.

Cafe 클래스의 addMusic 메소드 안에서 익명 클래스 만들기

public class Cafe {

    List<String> musicList = new ArrayList<>();

    public List<String> getMusicList() {
        return musicList;
    }

    interface Music {
            public void addMusic();
    }

    public void addMusic(String musicName, String singerName) {

        int listNumber = 1;

        //익명 클래스가 만들어지는 부분이다.
        Music cafeMusic = new Music() {

            @Override
            public void addMusic() {
                musicList.add(musicName);
                System.out.println(listNumber + "번 리스트에 음악을 추가합니다.");
            }
        };

        cafeMusic.addMusic();
    }

    public static void main(String[] args) {
        Cafe cafe = new Cafe();
        cafe.addMusic("The Only Exception","paramore");

        System.out.println(cafe.getMusicList().get(0));
    }
}

익명 클래스 표현식은 생성자를 호출하여 인스턴스를 생성하는 것과 매우 유사하다.

익명 클래스 표현식은 다음으로 구성된다.

  • new연산자
  • 구현할 인터페이스 또는 상속받을 클래스의 이름. (위 예제에서 익명 클래스는 Music인터페이스를 구현하였다 .)
  • 일반 클래스의 인스턴스 생성 표현식과 같이 생성자에 대한 인수를 포함하는 괄호.
  • 중괄호의 클래스 선언 본문.
new Music() {
    //body
}

다른 특징은 로컬 클래스와 다르지 않지만(둘러싼 클래스의 멤버에 접근 가능, 변수 캡쳐, static 제한) 익명 클래스에서는 생성자를 정의할 수 없다.

        Music cafeMusic = new Music() {

            //public Music() {}; //컴파일 에러

            @Override
            public void addMusic() {
                musicList.add(musicName);
                System.out.println(listNumber + "번 리스트에 음악을 추가합니다.");
            }
        };

보통 익명 클래스는 상속하거나 구현하는 메소드가 두 개 이상인 경우 사용한다. 하나의 메소드만 구현하는 경우에는 람다식을 사용하면 더 간결하게 코딩할 수 있다.


람다식 사용법

람다식

익명 클래스 사용의 단점은 익명 클래스에 단 하나의 메소드만 포함하는 경우에 구현이나 기능은 매우 간단한 반면 가독성 부분에서 좋지 않다는 점이다. 이럴 때 람다식을 사용하면 단일 메서드 클래스의 인스턴스를 보다 간결하게 표현할 수 있다.

또한 람다식은 자바에서 순수하게 함수만을 구현하고 호출하기 위한 함수형 프로그래밍을 구현하기 위한 표현식이다. 람다식을 통해서 클래스를 생성하지 않고 함수를 호출하는 것 만으로 기능을 수행한다. (내부적으로 익명 객체를 사용) 함수 내에서는 매개변수만 사용하기 때문에 외부에 사이드 이펙트를 주지 않고 병렬 처리 가능하여 안정적이고 확장성있는 프로그래밍 방식이다.

람다식은 익명 클래스에 비해 훨신 간결하게 표현된다.

    interface Music {
            public void addMusic();
    }

    public void addMusic(String musicName, String singerName) {

        int listNumber = 1;

        Music cafeMusic = new Music() {

            @Override
            public void addMusic() {
                musicList.add(musicName);
            }
        };

        cafeMusic.addMusic();
    }
    interface Music {
            public void addMusic();
    }

    public void addMusic(String musicName, String singerName) {

        int listNumber = 1;

        //람다식으로 인스턴스 생성
        Music cafeMusic = () -> musicList.add(musicName);

        cafeMusic.addMusic();
    }

람다식 사용법

람다식의 구성

  • 타입, 매개변수 목록 (괄호).

        interface Music {
            public boolean addMusic(String name);
        }
    Music cafeMusic = (String n) -> musicList.add(n);

    참고 : 람다 식에서 파라미터의 데이터 타입을 생략 할 수 있다. 또한 파라미터가 하나만 있는 경우에는 괄호를 생략 할 수 있다.

    Music cafeMusic = n -> musicList.add(n);
  • 화살, ->

  • 단일 표현식 또는 명령문 블록으로 구성된 본문. List.add() 메소드가 boolean을 반환하고 addMusic의 return으로 전달된다.

    n -> musicList.add(n);

    또는 return 을 사용할 수 있다. return은 표현식이 아니므로 명령문 블록으로 처리해야 한다.

    n -> {
            return musicList.add(n);
          };

람다식의 변수 캡쳐

람다식도 로컬 클래스와 마찬가지로 바깥쪽 클래스의 멤버에 접근하거나 final 또는 사실상 final 인 지역 변수에 접근할 수 있다.

    interface Music {
        public void addMusic();
    }

    public void addMusic(String musicName, String singerName) {

        int listNumber = 1;

        //람다식
        Music cafeMusic = () -> {
            //바깥쪽 클래스의 멤버 변수 musicList 접근
            musicList.add(musicName);
            //listNumber는 사실상 final 이어야 하므로 변경할 수 없다.
            //listNumber += 1;
            //listNumber는 final로 선언되지 않았지만 변경되지 않았으므로 '사실상 final'이다.
            System.out.println(listNumber + "번 리스트에 음악을 추가합니다.");
        };

        cafeMusic.addMusic();
    }

람다식은 메소드의 구현과 매우 유사하다. 람다식은 이름 없는 메소드라고 생각해도 된다.

interface PrintString{
    void showString(String str);
}

public class TestLambda {

    public static void showMyString(PrintString p) {
        p.showString("Test2");
    }

    public static PrintString returnString() {
        return str->System.out.println(str+"!!!"); //이름 없는 메소드를 반환
    }

    public static void main(String[] args) {

        PrintString lambdaStr = str -> System.out.println(str); //이름 없는 메소드를 변수에 대입
        lambdaStr.showString("Test1");

        showMyString(lambdaStr); //매개 변수로 활용
        PrintString lambdaStr2 = returnString(); //반환된 메소드를 변수에 대입
        lambdaStr2.showString("Test3");
    }
}

함수형 인터페이스

람다 표현식으로 구현이 가능한 인터페이스는 추상 메서드를 1개만 가지고 있는 인터페이스만 가능하며 그렇기 때문에 추상 메서드가 1개인 인터페이스를 부르는 명칭이 추가됐다. 그것이 함수형 인터페이스이다.

@FunctionalInterface
interface Music {
    public void addMusic();
}

@FunctionalInterface 애노테이션은 해당 인터페이스가 함수형 인터페이스라는걸 알려준다. 추상메서드가 1개가 아닐 경우 컴파일 에러가 발생한다. 이 인터페이스는 람다식에 사용되기 때문에 단 하나의 추상 메소드만 가지고 있어야 한다는 것을 알리기 위해 붙여주는 것이 좋다.

@FunctionalInterface이 안붙어있다고 해서 함수형 인터페이스로 동작하지 않는 것은 아니다. @FunctionalInterface 애노테이션은 인터페이스가 함수형 인터페이스라고 확실하게 정해둘 때 사용하고 컴파일러가 함수형 인터페이스의 요구 조건에 맞지 않을 시 에러를 주게 하기 위해서 쓰는 것이지 함수형 인터페이스들이 꼭 @FunctionalInterface 애노테이션으로 선언되어야 할 필요는 없다.

java.util.function 패키지에는 자바가 제공하는 여러가지 함수형 인터페이스들이 있다.

https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html

예를들어 BinaryOperator<T> 함수형 인터페이스는 동일한 유형의 두 피연산자에 대한 연산을 나타내며 피연산자와 동일한 타입의 결과를 반환한다.

public class MyOperatorTest {

    public static Integer operate(Integer num1, Integer num2, BinaryOperator<Integer> operator) {
        return operator.apply(num1, num2);
    }

    public static void main(String[] args) {
        BinaryOperator<Integer> operator = (num1, num2) -> num1+num2;
        System.out.println(operate(3, 5, operator)); //8
    }
}

메소드, 생성자 레퍼런스

람다식은 간단하게 기존에 선언되어 있는 메소드를 호출하는 기능만 가지고 있는 경우가 있는데, 이럴 때는 메소드 레퍼런스를 사용하여 기존 메소드를 이름으로 참조하는 것이 가능하며 보기에 더 명확하고 간결하게 코딩할 수 있다.

종류
정적 메소드 레퍼런스 ContainingClass::staticMethodName
특정 객체의 인스턴스 메소드 레퍼런스 containingObject::instanceMethodName
특정 타입의 임의 객체의 인스턴스 메서드 레퍼런스 ContainingType::methodName
생성자 레퍼런스 ClassName::new
class Music {

    public void download() {
        System.out.println("Download Music");
    }
}

class CafeMusic extends Music{

    List<String> musicList = Arrays.asList("Perm", "Dolphin", "Piano Man");

    public CafeMusic() {}

    public void showList() {
        for (String s : musicList) {
            System.out.println(s);
        }
    }

    public static void on() {
        System.out.println("Music On");
    }
}

public class Cafe {

    public void setUpMusic(CafeMusic cafeMusic) {

        //특정 타입의 임의 객체의 인스턴스 메서드 레퍼런스
        //ContainingType::methodName
        //Runnable runnable1 = () -> cafeMusic.download();
        Runnable runnable1 = cafeMusic::download;
        runnable1.run();

        System.out.println("=================");

        //특정 객체의 인스턴스 메소드 레퍼런스
        //containingObject::instanceMethodName
        //Runnable runnable2 = () -> cafeMusic.showList();
        Runnable runnable2 = cafeMusic::showList;
        runnable2.run();

        System.out.println("=================");

        //정적 메소드 레퍼런스
        //ContainingClass::staticMethodName
        //Runnable runnable3 = () -> CafeMusic.on();
        Runnable runnable3 = CafeMusic::on;
        runnable3.run();

    };

    public static void main(String[] args) {

        Cafe cafe = new Cafe();

        //생성자 레퍼런스
        //ClassName::new
//        Supplier<CafeMusic> supplier = () -> {
//            return new CafeMusic();
//        };
        Supplier<CafeMusic> supplier = CafeMusic::new;

        cafe.setUpMusic(supplier.get());
    }
}

참고 : https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html

Comments