ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 자바 내부 클래스, 람다식, 함수형 프로그래밍, 함수형 인터페이스
    언어/Java 2023. 7. 13. 23:46
    728x90
    반응형
    SMALL

    내부 클래스(inner class)

    • 클래스 내부에 선언한 클래스로 이 클래스를 감싸고 있는 외부 클래스와 연관된 경우이거나, 다른 외부 클래스에서는 사용하지 않을 경우 선언됩니다.
    • 중첩 클래스라고도 합니다.

     

    인스턴스 내부 클래스

    • 내부적으로 사용할 클래스를 선언하여 사용합니다. (private으로 선언하는 것이 권장됩니다.)
    • 외부 클래스가 생성된 후 생성됩니다.
    • 정적 변수나 정적 메서드 선언이 불가합니다. (클래스가 정적 클래스가 아니므로 인스턴스 변수, 메서드만 사용 가능합니다.)
    class Outer {
    
        int outerMember;
        private Inner innerMember;
    
        public Outer(){
            innerMember = new Inner();
        }
    
        public useInner(){
            innerMember.print();
        }
    
        class Inner{
            int innerMember = 1;
    
            void print(){
                System.out.println(innerMember);
            }
        }
    }
    
    ...
    
    Outer outer = new Outer();
    outer.useInner(); // 1출력
    
    Outer.Inner inner = new Outer.Inner();
    inner.print(); // 1출력

     

    정적 내부 클래스

    • 외부 클래스와 무관하게 사용할 수 있습니다.
    • 정적 변수, 정적 메서드가 사용 가능합니다.
    class Outer{
    
        ...
    
        static class Inner{
            int num = 1;
            static int staticNum = 2;
    
            void print(){
                System.out.println(num);
                System.out.println(staticNum);
            }
    
            static void staticPrint(){
                System.out.println(staticNum);
            }
        }
    }
    
    
    Outer.Inner inner = new Outer.Inner();
    inner.print(); // 1,2 출력
    inner.staticPrint(); //2 출력
    Outer.Inner.staticPrint(); // 2 출력
    System.out.println(Outer.Inner.staticNum); // 2 출력

     

    정적 내부 클래스의 메서드의 멤버 변수 접근 가능 여부에 대해 정리해보도록 하겠습니다.

    정적 내부 클래스 메서드의 멤버 변수 접근 가능 여부

     

    지역 내부 클래스

    • 지역 변수와 같이 메서드 내부에서 정의하여 사용하는 클래스입니다.
    • 메서드의 호출이 끝나면 메서드에 사용된 지역변수의 유효성은 사라집니다.
    • 메서드 호출 이후에도 사용해야 하는 경우가 있으므로, 지역 내부 클래스에서 사용하는 메서드의 지역 변수나 매개 변수는 final로 선언됩니다.
    • 직접 생성할 경우 메서드를 통해 인스턴스를 반환받아야 합니다.
    Class Outer {
    
        InnerInterface getInner(int num){
    
            int n = 1;
    
            class Inner implements InnerInterface{
    
                //지역 내부 클래스는 num과 n을 상수로 취급하기 때문에 새로운 값을 할당할 수 없습니다
                int localNum = 2;
    
                @Override
                public void print(){
                    System.out.println(localNum);
                    System.out.println(n);
                    System.out.println(num);
                }
            }
    
            return new Inner();
        }
    }
    
    Outer outer = new Outer();
    InnerInterface inner = outer.getInner(4);
    inner.print(); //2,1,4 출력

     

    익명 내부 클래스

    • 이름이 없는 클래스입니다.
    • 클래스의 이름을 생략하고 주로 하나의 인터페이스나 하나의 추상 클래스를 구현하여 반환합니다.
    • 인터페이스나 추상 클래스 자료형의 변수에 직접 대입하여 클래스를 생성하거나 지역 내부 클래스의 메서드 내부에서 생성하여 반환할 수 있습니다.
    • widget의 이벤트 핸들러에 활용됩니다.
    Class Outer{
    
        InnerInterface getInner(){
    
            return new InnerInterface(){
    
                @Override
                public void print(){
                    System.out.println("리턴 받은 익명 클래스");
                }
            }
        }
    
        InnerInterface inner = new InnerInterface() {
    
            @Override
            public void print(){
                System.out.println("지역 변수로 선언된 익명 클래스");
            }
        }
    }
    
    Outer outer = new Outer();
    InnerInterface inner = outer.getInner();
    inner.print(); //리턴 받은 익명 클래스
    outer.inner.print(); //지역 변수로 선언된 익명 클래스

     

    함수형 프로그래밍 & 람다식

    • 함수의 구현과 호출만으로 프로그래밍이 수행되는 방식입니다.
    • 순수함수를 호출함으로써 외부 자료에 부수적인 영향(side effect)를 주지 않도록 구현하는 방식입니다. (순수 함수란 매개변수만을 사용하여 만드는 함수입니다. 즉, 순수 함수 내부에서는 외부에 있는 변수를 사용하지 않아 순수 함수가 수행되더라도 외부에는 영향을 미치지 않습니다.)
    • 함수를 기반으로 하는 프로그래밍이고 입력 받은 자료 이외에 외부 자료를 사용하지 않아 여러 자료가 동시에 수행되는 병렬 처리가 가능합니다.
    • 함수의 기능이 자료에 독립적임이 보장됩니다. (동일한 자료에 대해 항상 동일한 결과를 보장하고, 다양한 자료에 대해 같은 기능을 수행할 수 있습니다.)
    • 자바 8부터 함수형 프로그래밍 방식을 지원하고 이를 람다식이라고 합니다.

     

    람다식 문법

    • 익명 함수를 만들어주는 것입니다.
    • 매개 변수를 이용한 실행문은 (매개변수) -> {구현물;} 입니다.
    (int x, int y) -> {return x+y;} // 두 매개 변수와 반환 값이 존재한 람다식
    
    x -> {System.out.println(x);} // 매개 변수가 한 개일 경우 자료형과 괄호 생략 가능
    
    x -> System.out.println(x); //실행문이 한 문장일 경우 중괄호 생략 가능
    
    (x,y) -> x+y; // 실행문이 한 문장일 경우 return과 중괄호 모두 생략 가능
    (x,y) -> return x+y; // 중괄호만 생략은 불가능

     

    함수형 인터페이스

    • 람다식을 선언하기 위한 인터페이스입니다.
    • 익명 함수와 매개 변수만으로 구현되는 인터페이스는 단 하나의 메서드만을 선언해야 합니다.
    • @FunctionalInterface 어노테이션을 붙여줍니다.
    @FunctionalInterface 
    interface MyAdd{
        int add(int x,int y);
    }
    
    MyAdd ma = (x,y)->(x+y)*2; // 람다식으로 인터페이스 구현
    System.out.println(ma.add(3,4)); //14 출력

     

    구현의 차이

    아래의 인터페이스를 구현하여 사용할 때, 객체 지향 프로그래밍과 람다식 표현을 이용한 방식을 보여드리도록 하겠습니다.

    interface MyAdd{
        int add(int x,int y);
    }

     

    객체 지향 프로그래밍

    class OOPAdd implements MyAdd{
    
        @Override
        public int add(int x, int y) {
            return (x+y)*2;
        }
    }
    
    OOPAdd oopAdd = new OOPAdd();
    System.out.println(oopAdd.add(3,4)); // 14출력

     

    람다식 이용

    MyAdd lambdaAdd = (x, y) -> (x+y)*2;
    System.out.println(lambdaAdd.add(3,4)); // 14출력

     

    클래스를 이용할 경우와 람다식을 이용하는 경우 중 더 좋은 방법은 없다고 생각합니다.

    람다식을 사용할 경우 직관적으로 어떤 함수를 구현했고 코드의 흐름을 알기 쉽다면, 객체 지향 프로그래밍을 사용했다면 add라는 함수를 다른 클래스 상에서 오버라이딩하여 사용할 경우 다형성을 이룰 수 있기 때문에 코드의 재사용성이 증가하고 유지보수에 용이합니다.

    이처럼 어떤 방식이 더 좋다기 보다는 자신의 상황에 맞는 프로그래밍 기법을 찾아서 사용해야 된다고 생각합니다.

     

     

    Java 기본 제공 함수형 인터페이스

    함수형 인터페이스 Descriptor Method
    Predicate T -> boolean boolean test(T t)
    Consumer T -> void void accept(T t)
    Supplier () -> T T get()
    Function<T, R> T -> R R apply(T t)
    Comparator (T, T) -> int int compare(T o1, T o2)
    Runnable () -> void void run()
    Callable () -> T V call()

     

    위와 같은 함수형 인터페이스를 기본적으로 제공해주고 있습니다.

    위와 같은 메서드가 필요할 경우 해당 인터페이스의 메서드를 구현해서 사용한다면 더 간편하게 사용할 수 있을 것 같습니다.

     

    예 1)

    class A<T> implements Predicate<T>{
    	
        T x;
        
        A(T x){
        	this.x = x;
        }
        
        @Override
        public boolean test(T t) {
        	return x.equals(t);
        }
    }
    
    A<String> a = new A<String>("aaa");
    System.out.println(a.test("bbb")); // false 반환

     

    위와 같이 Predicate 인터페이스의 test 메서드를 구현했을 경우 boolean 타입을 반환하는 메서드를 구현해서 사용할 수 있습니다.

    (위와 같이 구현할 경우 test 메서드를 굳이 인터페이스로 구현하는 것에 대한 이유가 궁금하시다면 인터페이스를 구현하는 이유에 대해 한 번 알아보시는 것도 좋을 것 같습니다.)

     

    예 2)

    Predicate<String> p = (String str) -> str.equals("aaa");
    System.out.println(p.test("aaa")); // true 출력

     

    위의 함수형 인터페이스의 경우 제네릭 함수형 인터페이스입니다. 자바는 기본적인 타입에 대한 함수형 인터페이스 또한 제공해주고 있으므로 타입에 맞게 사용할 수 있습니다.

    IntPredicate // (int t) -> boolean
    LongPredicate // (long t) -> boolean
    
    ...
    
    IntConsumer // (int t) -> void
    ...

     

    익명 객체(by 람다식)

    • 람다식으로 익명 내부 클래스를 생성함과 동시에 인스턴스를 생성해줍니다.
    // 첫 번째 방식
    MyAdd lambdaAdd = new MyAdd() {
    
        @Override
        public int add(int x, int y){
            return (x+y)*2;
        }
    }
    
    System.out.println(lambdaAdd.add(3,4)); // 14출력

     

    함수 변수

    아래의 함수형 인터페이스를 여러 변수처럼 사용해보도록 하게습니다.

    interface MyAdd{
        int add(int x,int y);
    }

     

    • 인터페이스형 변수에 람다식 대입
    MyAdd ma = (x,y) -> x+(y*2)+3;
    System.out.println(ma.add(3,4)); // 14 출력

     

    • 매개변수로 전달
    void printAdd(MyAdd ma){
        System.out.println(ma.add(3,4));
    }
    
    
    MyAdd ma = (x,y) -> x+(y*2)+3;
    printAdd(ma); // 14 출력

     

    • 반환 값으로 사용
    MyAdd getAdd(){
        return (x,y) -> x*2+y*3;
    }
    
    MyAdd ma = getAdd();
    System.out.println(ma.add(3,4)); // 18 출력

     

     

     

    오늘은 자바 내부 클래스에 대해 알아보았습니다. 내부 클래스와 같은 경우 외부 클래스와 연관성이 높고 다른 외부 클래스에서는 사용하지 않는 클래스의 경우 사용 할 것 같습니다. 정적, 기본형 메서드를 통해 접근할 수 있는 멤버 변수에 대해서도 제대로 활용할 수 있다면 안정적인 프로그래밍을 할 수 있을 것 같습니다.

    또한, 함수형 프로그래밍에 필요한 람다식에 대해 알 수 있었습니다. 람다식과 함수형 인터페이스를 이용해 간결하고 가독성 높은 코드를 작성할 수 있을 것 같습니다. 또한, 순수 함수로 선언하고 사용하다면 부수 효과가 없는 항상 같은 결과값을 냄으로써 안정성을 높일 수 있습니다.

    어떻게 보면 객체 지향 프로그래밍을 기본으로 사용하는 자바에서 람다식 문법을 통해 함수를 변수처럼 사용할 수 있도록 해주고 재사용성을 높여줌으로써 함수형 프로그래밍을 할 수 있도록 해준 것이 의문이 들 수도 있습니다. 이 부분은 위의 글 중간에서도 말했듯이 프로그래밍 기법에는 언제나 통하는 만능 정답이 있는 것이 아니라 상황에 따라 더 적합한 기법을 적용하는 것이기 때문에, 두 기법에 대해 잘 알고 필요에 따라 사용할 수 있도록 연습하는 것이 중요하다고 생각합니다.

    반응형
    LIST

    댓글

Designed by Tistory.