본문 바로가기

JAVA

람다식

학습

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

 

람다식을 사용하기에 앞서 익명 구현 객체라는 것에 대해 알면 좋습니다.

익명 구현 객체는 인터페이스나 클래스의 객체를 생성해서 사용할 때, 재사용하지 않는 경우 보통 사용합니다.

 

예를 들어 보겠습니다.

특정 인터페이스를 사용하기 위해 이 인터페이스를 상속 받은 클래스를 구현하는 방법이 있습니다.

public class Exam_001 {
    public static void main(String[] args) {
        Exam_001_Sub exam_001_sub = new Exam_001_Sub();
        exam_001_sub.doSomething();
    }
}

interface AnoymousTest_001 {
    void doSomething();
}

// 인터페이스를 구현한 클래스 정의
class Exam_001_Sub implements AnoymousTest_001 {
    @Override
    public void doSomething() {
        System.out.println("do something!!")
    }
}

[출력]
do something!!

 

그런데 만약 Exam_001_Sub 클래스가 AnoymousTest 인터페이스의 구현체로만 사용되고, Exam_001_Sub 클래스가 재사용되지 않는다고 하면 새로운 클래스 파일을 생성해서 관리하는게 부담될 수 있습니다.

 

그래서 다음과 같이 별도의 클래스를 작성하지 않고 인터페이스를 바로 구현하여 사용하는 방법이 있는데, 이것을 익명 구현 객체라고 부릅니다. (작성하지는 않지만 별도의 클래스 파일이 생성됩니다. 본문의 아래쪽에서 다루겠습니다.)

public calss Exam_002 {
    public static void main(String[] args) {
        AnoymousTest_002 anonymousTest_02 = new AnonymousTest_002() {
            @Override
            public void doSomething() {
                System.out.println("do something!!");
            }
        };
        anoymousTest_002.doSomething();
    }
}

interface AnoymousTest_002 {
    void doSomethin();
}

[출력]
do something!!

 

람다식 얘기에 앞서 익명 구현 객체에 대해 언급한 이유는 람다식이 이 익명 구현 객체와 생긴게 비슷하기 때문입니다. 그래서 간혹 람다식이 간결하게 표현한 익명 구현 객체 처럼 생각되지만 실제로 생성되는 코드를 보면 전혀 다르다는 것을 알 수 있습니다. 자세한 내용은 뒤쪽에 정리해 두었습니다.

 

  • 함수형 인터페이스

람다식의 사용 방법에 정리하기에 앞서 함수형 인터페이스에 대해 알아야 합니다.

말이 거창해서 그렇지 결론부터 이야기하면 추상메소드가 하나뿐인 인터페이스를 함수형 인터페이스라고 합니다.

 

즉, 다음과 같은 인터페이스들을 모두 함수형 인터페이스에 속합니다.

public class Exam_003 {
}

interface FunctionalInterface_001 {
    void aaa();
}

// 함수형 인터페이스는 @FunctionalInterface를 사용해 명시적으로 컴파일러에게 알려줄 수 있습니다.
// @FunctionalInterface를 사용하면 다른 사람이 추상 메소드를 추가하는 상황을 예방할 수 있습니다.
@FunctionalInterface
interface FunctionalInterface_002 {
    void aaa();
    
    // static 메소드가 있어도 괜찮습니다.
    staticc void bbb() { }
    
    // default 메소드가 있어도 괜찮습니다.
    default void ccc() { }
}

위에 선언한 두 개의 인터페이스의 특징은, 이 인터페이스를 상속 받은 클래스가 반드시 구현해야하는 추상 메소드가 

한 개뿐이라는 것입니다.

 

추가로 오버라이드 한 메소드에 대해 @Override 어노테이션을 붙여주는 것을 기억해야 합니다.

이 어노테이션은 작성하지 않아도 동작하는데 문제는 없지만

붙여 줌으로써 컴파일 타임에 Override 된 것이 맞는지 확인하는 것과 코드를 읽는 입장에서 재정의 된 메소드임을 인지할 수 있습니다.

 

비슷한 맥락에서 함수형 인터페이스도 @FunctionalInterface라는 어노테이션을 붙여줄 수 있습니다.

이 어노테이션을 붙이면 컴파일러가 해당 인터페이스에 추상 메소드 1개만 선언 되었는지 확인해주며 다른 사람이 이 코드를 보고 추상 메소드를 추가하는 문제를 예방할 수 있습니다.

 

만약 이 어노테이션이 작성 되어있는데, 복수개의 추상메소드가 정의되어 있다면 InteliJ IDE 기준으로 다음과 같은 메시지를 보여줍니다.

 

"Multiple non-overriding abstract methods found in interface me.xxxelppa.study.week.~~~~~"

 

 

  • 람다식 사용법

람다식은 생략 가능한 부분이 많기 때문에 헷갈릴 수 있지만, 기본적으로 화살표를 기준으로 좌측에 매개변수가 우측에 실행 코드가 작성됩니다.

 

(매개변수) -> {실행코드}

위의 형태를 기준으로 String 타입의 문자열을 받아서 그대로 출력하는 경우를 생각해봅시다.

public class Exam_004 {
    public static void main(String[] args) {
        
        // 1. 작성 가능한 모든 내용을 생략 없이 작성한 경우
        LambdaTest_001 mytest_01 = (String param) -> {
            System.out.println(param)
        };
        
        // 2. 매개변수의 타입을 생략한 경우
        LambdaTest_001 myTest_02 = (param) -> {
            System.out.println(param);
        };
        
        // 3. 매개변수가 한개여서 소괄호를 생략한 경우
        LambdaTest_001 myTest_03 = param -> {
            System.out.println(param);
        };
        
        // 4. 실행 코드가 한 줄이어서 중괄호를 생략한 경우
        LambdaTest_001 myTest_04 = (String param) -> System.out.println(param);
        
        // 5. 매개변수가 한개이고 실행코드가 한줄이어서 생략 가능한 모든 것을 생략한 경우
        LambdaTest_001 myTest_05 = param -> System.out.println(param);
        
        // 6. 반환 값이 있는 경우 return 키워드 사용하는 경우
        LamdaTest_002 myTest_06 = (n1, n2) -> {
            System.out.println("6. 반환값이 있는 경우 return 키워드 사용하는 경우");
            return n1 + n2;
        };
        
        // 7. 실행 코드가 반환 코드만 존재하는 경우 키워드와 중괄호 생략한 경우
        LambdaTest_002 myTest_07 = (n1, n2) -> n1 + n2;
        
        // 8. 매개변수가 없어서 소괄호를 생략할 수 없는 경우
        LambdaTest_003 myTest_08 = () -> System.out.println(" 8. 매개변수가 없어서 소괄호를 생략할 수 없는 경우");
        
        myTest_01.printString("1. 작성가능한 모든 내용을 생략 없이 작성한 경우");
        myTest_02.printString("2. 매개변수의 타입을 생략한 경우");
        myTest_03.printString("3. 매개변수가 한개여서 소괄호를 생략한 경우");
        myTest_04.printString("4. 실행 코드가 한줄이엿 중괄호를 생략한 경우");
        myTest_05.printString("5. 매개변수가 한개이고 실행코드가 한줄이어서 생략 가능한 모든것을 생략한 경우");
        
        System.out.println();
        System.out.println(myTest_06.add(10, 20));
        System.out.println(myTest_07.add(20, 30));
        
        myTest_08.noArgs();
    }
}

@FunctionalInterface
interface LambdaTest_001 {
    void printString(String str);
}

@FunctionalInterface
interface LambdaTest_002 {
    int add(int num1, int num2);
}

@FunctionalInterface
interface LambdaTest_003 {
    void noArgs();
}

 

  1. 작성 가능한 모든 내용을 생략 없이 작성한 경우
  2. 매개변수의 타입을 생략한 경우
  3. 매개변수가 한개여서 소괄호를 생략한 경우
  4. 실행코드가 한 줄이어서 중괄호를 생략한 경우
  5. 매개변수가 한 개이고 실행코드가 한 줄이어서 생략 가능한 모든 것을 생략한 경우
  6. 반환 값이 있는 경우
  7. return 키워드 사용하는 경우
  8. 매개변수가 없어서 소괄호를 생략할 수 없는 경우

람다식을 사용할 때 타켓 타입이라는 것이 존재합니다.

타겟 타입이란, 람다식은 기본적으로 '인터페이스 변수'에 담기는데, 이 람다식이 담기는 인터페이스를 타겟 타입이라고 합니다.

 

즉, 위에 작성한 예제 코드를 기준으로 LambdaTest_001과 LambdaTest_002를 타겟 타입이라고 할 수 있습니다.

 

람다식을 사용할 때 타겟 타입이 중요한 이유는 위에서 봐서 알겠지만, 람다식만 보고는 이게 어떤 함수형 인터페이스를 구현한 것인지 유추하기 힙듭니다.

 

() -> System.out.println("who am i ...?");

이것을 보고 void 타입의 매개변수가 없는 추상 메소드를 구현했다는 것은 알 수 있지만, 정확히 어떤 타겟 타입을 사용한 것인지(어떤 함수형 인터페이스를 사용 했는지) 모호하기 때문입니다.

 

 

추가로 람다식 관련하여 자바 표준 API가 존재합니다.

필요한 경우 직접 함수형 인터페이스를 정의해서 사용해도 무관하지만, 자주 사용하는 형태에 대해 표준으로 정의해서 제공하고 있으니 특병한 경우가 아니라면 직접 인터페이스를 정의해서 사용하는 일은 많지 않을 것입니다.

 

한빛미디어, 이것이 자바다 (참조)

 

  • Variable Capture

람다식의 실행 코드 블록 내에서 클래스의 멤버 필드와 멤버 메소드, 그리고 지역 변수를 사용할 수 있습니다.

클래스의 멤버 필드와 멤버 메소드는 특별한 데약 없이 사용 가능하지만, 지역변수를 사용함에 있어서는 제약이 존재합니다. 이 내용을 잘 이해하기 위해서는 JVM의 메모리에 대해 조금은 알아야 합니다.

 

잠시 람다식이 아닌 다른 이야기를 해보겠습니다.

멤버 메소드 내부에서 클래스의 객체를 생성해서 사용할 경우 다음과 같은 문제가 있습니다.

 

익명 구현 객체를 포함해서 객체를 생성할 경우 new라는 키워드를 사용합니다. 

이 키워드를 사용한다는 것은 동적 메모리 할당 영역(이하 heap)에 객체를 생성한다는 것을 의미합니다.

 

이렇게 생성된 객체는 자신을 감싸고 있는 멤버 메소드의 실행이 끝난 이후에도 heap 영역에 존재하므로 사용할 수 있지만, 이 멤버 메소드에 정의 된 매개변수나 지역 변수는 런타임 스택 영역(이하 stack)에 할당되어 메소드 실행이 끝나면 해당 영역에 사라져 더 이상 사용할 수 없게 됩니다.

 

그렇게 때문에 멤버 메소드 내부에서 생성된 객체가 자신을 감싸고 있는 메소드의 매개변수나 지역변수를 사용하려 할 때 문제가 생길 수 있습니다.

 

조금 더 쉽게 처음부터 설명하면 

 

1. 클래스의 멤버 메소드의 매개변수와 이 메소드 실행 블록 내부의 지역 변수는 JVM의 stack에 생성되고

메소드 실행이 끝나면 stack에서 사라집니다.

 

2. new 연산자를 사용해서 생성한 객체는 JVM의 heap 영역에 객체가 생성되고 GC(Garbage Collector)에 의해 관리되며, 더 이상 사용하지 않는 객체에 대해 필요한 경우 메모리에서 제거됩니다.

 

heap에 생성된 객체가 stack의 변수를 사요하려고 하는데, 사용하려는 시점에 stack에 더 이상 해당 변수가 존재하지 않을 수 있습니다. 왜냐하면 stack은 메소드 실행이 끝나면 매개변수나 지역변수에 대해 제거하기 때문입니다. 그래서 더 이상 존재하지 않는 변수를 사용하려 할 수 있기 떄문에 오류가 발생합니다.

 

 

자바는 이 문제를 Variable Capture라고 하는 값 복사를 사용해서 해결하고 있습니다.

 

즉, 컴파일 시점에 멤버 메소드의 매개변수나 지역 변수를 멤버 메소드 내부에서 생성한 객체가 사용할 경우 객체 내부로 값을 복사해서 사용합니다. 하지만 모든 값을 복사해서 사용 할 수 있는 것은 아닙니다.

여기에도 제약이 존재하는데 final 키워드로 작성 되었거나 final 성격을 가져야 합니다.

 

final 키워드로 작성 되는 것은 알겠는데, 성격을 가진다는 것은 무엇일까?

final 성격을 가진다는 것은 final 키워드로 선언된 것은 아니지만 값이 한번만 할당 되어 final 처럼 쓰이는 것을 뜻합니다.

 

(Java 1.7까지는 final을 반드시 명시 했어야 했고, final을 생략하고 쓸 수 있는건 Java 1.8부터 입니다.)

 

final 키워드 유무에 따라 값이 복사 되는 위치가 달라지는데, 다음과 같이 복사 됩니다.

public class Exam_005 {
    public void testMethod(final String myFinalString_01, String myString_01) {
        final String myFinalString_02 = "myFinalString_02";
        String myString_02            = "myString_02";
        
        class VariableCaptureTest {
            // final 명시하지 않은 멤버 필드로 복사  -- 7
            // String myString_01                    -- 8
            // String myString_02                    -- 9
            void print() {
                // final 명시한 경우 지역 변수로 복사  -- 11
                // String myFinalString_01             -- 12
                // String myFinalString_02             -- 13
                // System.out.println(myFinalString_01 + " :: " + myFinalString_02);
                // System.out.println(myString)01 + " :: " + myString_02);
            }
        }
        
        new VariableCaptureTest().print();
    }
    
    public static void main(String[] args) {
        Exam_005 exam_005 = new Exam_0005();
        exam_005.testMethod("myFinalString_01", "myString_01");
    }
}

7 ~ 9 라인, 11 ~ 13라인은 컴파일 시 생성 될 것으로 예상한 내용을 정리한 것입니다.

호기심이 생겨, 14, 15 라인을 사용할 때와 안할 때 생성되는 바이트 코드를 비교해 보겠습니다.

당장 눈에 띄는 것은 중첩 클래스 때문에 '외부 클래스' ${숫자}'내부_클래스'.class 클래스 파일이 추가로 생성 되었습니다.

 

비교 결과 값 복사를 확인할 수 있었는데, 예상했던 것과 조금 다른 결과가 나왔습니다.

 

1. final 키워드를 사용한 매개변수 : 값 복사가 일어납습니다.

2. final 키워드를 사용한 지역변수 : fianl 키워드가 사라지고 값 복사가 일어나지 않습니다..

3. final 키워드를 사용하지 않은 매개변수 : final 키워드가 생성되고 값 복사가 일어났습니다.

4. final 키워드를 사용하지 않은 지역 변수 : final 키워드가 생성되고 값 복사가 일어났습니다.

 

결과가 조금 이상한거 같아서 디 컴파일 된 결과가 아닌 바이트 코드를 열어 보았습니다.

 

자세한 내용은 모르겠지만 찾아보니 ALOAD 명령은 스택 영역에 값을 할당하는 명령인거 같아 보였습니다.

(확실하지는 않습니다...)

 

그리고 눈여겨 봐야 할 부분은 'NEW me/xxxeippa/study/week15/Exam_005$1VariableCaptureTest' 이 부분인데,

중첩 클래스에 대해 새로운 객체를 생성하는 것으로 보입니다.

 

이 주제의 마지막 부분에 람다식을 컴파일 한 결과가 어떤 결과가 나오는지 확인해볼 예정이니 꼭 기억했으면 좋겠습니다.

 

heap 영역에 존재하는 객체가 stack 영역의 변수를 안전하게 사용할 수 있도록 값 복사를 하는 것은 알겠습니다.

그럼 복사하는 변수는 왜 final 이어야 할까요?

final 이라는 것은 값을 변경할 수 없도록 하겠다는 것을 의미하는데, 값이 변경 가능하다면 문제가 생길 수 있기 때문입니다.

 

사본을 사용하고 있는데 원본을 외부에서 변경 할 수 있다면, 객체 내부에서 그 값을 마음 놓고 사용할 수 없기 때문입니다.

 

그러면 자연스럽게, 이런 멤버 메소드의 매개변수나 지역변수가 아닌, 인스턴스 변수에 대해서는 특별한 제약이 없다는 것에 대해 의문이 풀립니다.

왜냐하면 그런 인스턴스 변수들은 기본적으로 heap 영역에 존재하기 때문에, 위와 같이 별도로 값을 복사해서 사용할 필요 없이 직접 heap 영역에 접근해서 사용하면 되기 때문입니다.

 

그럼 다시 람다로 돌아가서, 일반적으로 람다식은 클래스의 멤버 메소드 내부에서 사용됩니다.

그렇기 때문에 람다식 내부에서 사용하는 외부 변수들에 대해 위에서 이야기한 동일한 문제가 발생하고, 이문제를 해결하기 위해 Variable Capture를 합니다.

 

궁금하니깐 간단한 람다식을 정의하고 컴파일 해보겠습니다.

public class Exam_011 {
    public static void main(String[] args) {
        
        MyFunctionalInterface mfi_1 = new MyFunctionalInterface() {
            @Override
            public void doProc() {
                System.out.println("익명 구현 객체");
            }
        };
        mfi_1.doProc();
        
        MyFunctionalInterface mfi_2 = () -> System.out.println("람다식");
       mfi_2.doProc();
    }
}

@FunctionalInterface
interface MyFunctionalInterface {
    void doProc();
}

[출력]
익명 구현 객체
람다식

 

익명 구현 객체에 대한 클래스 파일이 보이지 않아 직접 폴더를 찾아 확인해 보았습니다.

class Exam_011$1 implements MyFunctionalInterface {
    Exam_011$1() {
    }
    
    public void doProc() {
        System.out.println("익명 구현 객체");
    }
}

익명 구현 객체에 대해서는 클래스 파일이 별도로 생겼는데 람다식에 대한 클래스 파일은 생기지 않았습니다.

확인해보지 않았지만, 바로 위에서 살펴본 예제에서 중첩 클래스에 대해 NEW 키워드가 있었던 것으로 보아 이 코드도 바이트 코드를 확인해보면 같은 내용이 있을 거 같습니다.

 

그럼 람다식은 어떻게 동작하는 어떻게 동작하는 것일까? 굉장히 익명 구현 객체를 사용하는 것처럼 보였는데 실제 결과는 매우 달랐기 때문에 궁금해 바이트 코드를 열어 보았습니다.

 

이 코드의 바이트 코드를 보면 특이한 걸 볼 수 있습니다.

예상했던 대로 익명 구현 객체에 대해서는 NEW를 사용해서 객체도 생성 된것으로 보이고 또 별도의 클래스 파일도 생긴걸 확인했습니다.

 

람다식임 쓰인 부분에 대해서는 invokedynamic이라는 opcode를 사용했는데, java 1.8부터 생긴 것으로 interface의 default method와 lamda 식에서 사용된다고 합니다.

 

람다 내부 동작에 대해 참고가 된 글을 링크로 남기겠습니다.

tourspace.tistory.com/11

 

람다의 내부동작 #1

람다의 내부 자바의 lambda는 단순히 익명클래스로 치환되지 않습니다. bytecode를 확인하면 invokedynamic이란 opcode로 표현됩니다. 이번 포스팅에서는 람다의 내부동작은 어떤지, 왜 익명클래스로 치

tourspace.tistory.com

tourspace.tistory.com/12?category=788398

 

람다의 내부동작 #2

지난글에서 람다의 내부/외부 표현에 대한 글을 작성했습니다. 이번에는 람다의 내부적인 동작에 대한 상세한 이해를 위해 JVM 관련 부분과 bytecode 위주로 언급합니다. JVM의 opcode compile된 bytecode

tourspace.tistory.com

 

 

복잡하다면 익명 구현 객체를 사용할 때와 람다식을 사용했을 때 다음과 같은 차이점이 있다는 것만 기억하면 된다.

1. 람다식은 익명 구현 객체처럼 별도의 객체를 생성하거나 컴파일 결과 별도의 클래스를 생성하지 않는 다는 것입니다.

2. 람다식 내부에서 사용되는 변수는 Variable Capture(값 복사)가 발생하며, 이 값은 final이거나 final처럼 사용해야 합니다.

 

사실 사용하는 입장에서는 '인스턴스 변수를 제외하고 람다식 내부에서 사용하는 변수는 final이거나 final 성격을 가져야 야 한다'고 기억하면 별 문제가 없긴 합니다.

 

 

 

  • 메소드, 생성자 레퍼런스

메소드, 생성자 레퍼런스는 람다식을 더 간략하게 표현할 수 있게 해줍니다.

콜론 두개 :: 를 사용하면, 크게 다음과 같이 구분할 수 있습니다.

 

1. static 메소드 참조

-> 클래스_이름::메소드_이름

 

2. 인스턴스 메소드 참조

-> 인스턴스_변수::메소드_이름

 

3. 람다식의 매개변수로 접근 가능한 메소드 참조

-> 매개변수의_타입_클래스_이름::메소드_이름

 

4. 생성자 참조

-> 클래스_이름::new

 

로 사용할 수 있습니다.

 

각 경우에 대한 간단한 예시를 작성해보겠습니다.

우선 1, 2, 3번에 해당하는 예제는 다음과 같습니다.

 

 

다음은 생성자 참조의 예제입니다.

 

 

마지막으로 함수형 프로그래밍에 대해서 생각해봅시다.

 

1급 시민 또는 1급 객체(First-class citizen)

다음의 세 가지 조건을 모두 만족하는 것들을 말합니다.

1. 변수에 할당할 수 있습니다.

2. 매개변수로 사용할 수 있습니다.

3. 반환 값으로 사용할 수 있습니다.

 

당장 생각나는 대표적인 예는 javascript의 function 입니다.

갑자기 javascript 코드가 나와서 좀 그렇지만

javascript에서 함수는 변수에 담을 수 있고, 매개변수로 전달 가능하며, 반환 값으로도 사용할 수 있습니다.

그렇기 때문에 javascript에서 함수는 유명한 1급 시민 객체입니다.

 

다시 자바 얘기를 해봅시다.

혹시 지금까지 자바 코드를 작성하면서 메소드를 변수에 담아 전달해본 적이 있었을까?

메소드는 클래스에 종속되어 객체로 전달 하거나 객체를 반환한 적은 있어도, 메소드 자체를 전달해 본적은 없습니다.

 

그래서 자바의 메소드는 1급 시민 객체가 아닙니다.

 

하지만 자바의 람다식은 변수에 담을 수 있고, 매개변수로 전달할 수 있으며, 반환 값으로 사용할 수 있습니다.

람다식이 세가지 조건을 만족하기 때문에 1급 시민 객체인 것은 알겠는데, 그래서 뭐가 좋은걸까?

아펏 람다식의 Variable Capture에 대해 정리하면서 언급한 내용을 기억해야 합니다.

 

heap 영역에 생성된 객체가 stack 영역의 변수를 안정적으로 사용하기 위해 final 또는 final 성격을 가져야 합니다. 

즉, 변할 수 있는 것을 변하지 않도록 제한을 둔 것입니다. 이것을 불변 상태(Immutable)로 만든다고 합니다.

 

불변 상태로 만든다는 것에 대해 잘 이해가 되지 않는다면, 조금 더 쉬운 말로 '외부의 상태에 독립적'이라고 표현할 수 있습니다. 외부의 상태에 독립적이라는 것은 다른 말로 순수 함수라고 할 수 있는데, 다음의 javascript 코드를 봅시다.

 

실행 결과는 같지만, myPureFunction과 myFunction의 차이점은 실행결과가 외부 변수에 의해 실행 결과에 영향을 주는가 입니다.

 

Variable Capture를 통해 람다식 내부에서 사용하는 지역 변수에 대해 final 이어야 하는 이우도 같은 맥락이라고 할 수 있습니다. 

 

불변 상태로 만들면 지역 변수에 대해 변하지 않는 상수를 사용하기 때문에 동일한 입력에 대해 동일한 결과를 기대할 수 있습니다. 이것을 부작용(side effect, 부수효과)이 없다고 합니다.

 

동일한 입력에 대해 일관된 결과를 받아볼 수 있다는 것은 다시 말하면

다수의 쓰레드가 동시에 공유해서 사용한다고 하더라도 일관된 결과를 받아볼 수 있다는 것으로 쓰레드와 관련된 동시성 문제가 생길 원인을 미리 방지할 수 있습니다.

 

 

 

[참조]

blog.naver.com/hsm622/222260183401

 

15주차 과제: 람다식

# 목표 :: 자바의 대해 학습하세요. # 학습할 것 : 람다식 사용법 : 함수형 인터페이스 : Variable Captu...

blog.naver.com

 

'JAVA' 카테고리의 다른 글

자바 데이터 타입, 변수 그리고  (0) 2021.04.25
JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가?  (0) 2021.04.18
제네릭(Generic)  (0) 2021.02.27
I/O (Input/Output)  (0) 2021.02.20
어노테이션  (0) 2021.02.03