[After Java Semina] 내부클래스와 람다식

Jiwon-Woo·2021년 9월 11일
0

After Java Semina

목록 보기
3/5

1. 내부클래스

1.1 내부클래스의 정의와 유형

  • Nested class = 중첩 클래스
  • Static nested calss = 정적 내부(중첩) 클래스
  • Inner class = Instance Inner class = [Instance] Member class
    = 인스턴스 [내부] 클래스, 맴버 클래스
  • Anonymous inner class = Anonymous class = 익명 [내부] 클래스
  • Method local inner class = local class = 지역 [내부] 클래스

클래스 안에 클래스를 넣는 공통적인 이유(Nested, 중첩)

  • 특정 클래스에서만 사용되는 클래스를 논리적으로 묶기 위해
  • 특정 클래스에서만 사용 + private 프로퍼티에 직접적으로 접근 + 외부로부터 감추고 싶은 클래스(캡슐화)
  • 소스의 가독성과 유지보수성을 높이고 싶을 때(?)

Oracle nested class

1.2 인트턴스 내부 클래스

클래스 내부의 맴버 위치에 선언된 클래스.

  • 해당 클래스를 포함하는 클래스(이하 외부클래스) 부터 선언하고, 이후 내부 클래스를 선언하는 방식으로 사용한다.

    +) 내부 클래스는 외부 클래스가 생성된 이후에 사용해야하기 때문에 클래스 생성 여부와 상관없이 사용할 수 있는 정적 변수와 정적 메서드를 포함할 수 없다. 만약 포함하게 되면 컴파일 오류 발생.

  • 외부클래스에서 사용하려면 new를 통해 인스턴스화 이후 사용한다.

  • 외부 클래스의 멤버 모두 사용할 수 있다.

예제코드1

    package innerclass;

    public class OuterMember {
        private Integer id;
        private String name;
    		// 맴버들과 같은 위치에서 정의
        protected class **InnerMember** {
            String form;

            InnerMember() {
    						// 외부 클래스의 private라도 접근 가능
                form = id.toString() + name;
            }

            public void printInfo() {
                System.out.println(form);
            }
        }
    }
    package innerclass;

    public class TestInner {
        public static void main() {
            **OuterMember Om = new OuterMember();**
    				// 외부 클래스를 인스턴스화 이후 외부인스턴스.new를 통해 인스턴스화
            **OuterMember.InnerMember Im = Om.new InnerMember();**
        }
    }

예제코드2

    package innerclass;

    public class OutClass {
        private int num = 10;
        private static int sNum = 20;

        private InClass inClass;

        public OutClass() {
            inClass = new InClass();
        }

        class InClass {
            //static int inNum = 100;
    				int inNum = 100;
            void inTest() {
                System.out.println("OutClass의 인스턴스 변수 num : " + num);
                System.out.println("OutClass의 정적 변수 sNUM : " + sNum);
                System.out.println("InClass의 인스턴수 변수 inNum : " + inNum);
            }
        }

        public void usingClass() {
            inClass.inTest();
        }
    }
    package innerclass;

    public class InnerTest {
        public static void main(String[] args) {
            OutClass outClass = new OutClass();
            outClass.usingClass();
        }
    }

복잡함에도 불구하고 사용하는 이유 : 특정 클래스 내부에서는 자주 사용하지만 외부에서는 전혀 사용하지 않고, 동작을 숨기고싶은 클래스가 있을 때. 보통 GUI관련 프로그램 개발시 자주사용한다고 한다. GUI에서 이벤트(클릭 등)가 발생할 때 동작은 클래스마다 다른 경우가 많다. 이러한 이벤트들의 동작을 정의할 때 많이 사용한다.(리스너...?)

1.3 정적 내부(중첩) 클래스

클래스 내부에 static키워드로 선언된 클래스.

  • static 키워드를 사용하므로, 외부클래스의 정적 맴버만을 사용할 수 있다.
  • 패키지처럼 외부클래스.내부클래스형태로 접근하면 된다.
  • static키워드이기 때문에 외부 클래스와 별도로 객체를 생성할 수 있다.
  • 예제코드
    package staticnestedclass;

    public class OutterMember {
        private Integer id;
        private String name;

        static public class InnerMember {
            String form;

            public InnerMember (String form) {
                // Non-static field 'id' / 'name' cannot be referenced from a static context
                //form = id.toString() + name;
                this.form = form;
            }
        }
    }
    package staticnestedclass;

    public class TestInner {
        public static void main(String[] args) {
            OutterMember.InnerMember inner = new OutterMember.InnerMember("test");
            OutterMember outter = new OutterMember();
        }
    }

외부 클래스 에서만 사용되지만, 외부클래스와 독립적으로 존재하는 객체인 경우
static이기 때문에 정말 필요할때만 사용하는것이 좋다.

1.4 지역 [내부] 클래스

특정 메소드내부에서 선언되는 클래스.

  • 메소드 내부에서만 객체 생성 가능하며 메소드가 호출될때 생성되고 메소드가 종료될때 소멸한다.

    +) 만약 지역 내부 클래스에서 상위 메서드에서 선언된 변수가 사용되면 컴파일 시에 final 변수, 즉 상수로 변경된다. 그렇기 때문에, 상위 메서드의 변수는 지역 내부 클래스에서 값을 변경할 수 없다.

  • 메소드 외부에서는 접근이 불가능하다.

예제코드

    package localclass;

    public class Member {
        private Integer id;
        private String name;

        public void MemberMethod() {
            int num;
            // Cannot resolve symbol 'LocalClass'
            //LocalClass localClass = new LocalClass();

            // public 등 접근지정자 안됨
            class LocalClass {
                    private int number;
                    private String form;

                    // 외부 클래스의 프로퍼티에 접근 가능
                    LocalClass() {
                        form = id.toString() + name;
                        // Variable 'num' might not have been initialized
                        //number = num;
                    }
            }

            // 메소드 내부 + 클래스 정의 이후 인스턴스화 가능
            LocalClass localClass = new LocalClass();
        }
    }

예제코드2

    package innerclass;

    public class Outer {
        private int num = 10;
        private static int sNum = 20;

        void inTest() {
            int inNum = 100;

            class LocalInClass {
                int localNum = 42;

                public void print() {
                    System.out.println("localNum : " + localNum);
                    System.out.println("inNum : " + inNum);
                    localNum *= 2;
    //                inNum += 1;   // 컴파일 오류
                    System.out.println("localNum * 2 : " + localNum);
                }
            }

            new LocalInClass().print();
        }

        SquareInterface localTest() {
            class LocalClass implements SquareInterface {

                @Override
                public int square(int n) {
                    return n * n;
                }
            }
            return new LocalClass();
        }

    }
    package innerclass;

    public class LocalInnerTest {
        public static void main(String[] args) {
            Outer outClass = new Outer();

            outClass.inTest();

            SquareInterface square = outClass.localTest();
            System.out.println(square.square(9));
        }
    }

1.5 익명 [내부 / 중첩] 클래스

이름이 없는 클래스. 선언된 클래스 내부 선언되고 한번만 사용하는 클래스.

  • 특정 클래스를 상속하거나, 구현해야한다.
  • 재사용하지 않고 오로지 특정위치에서만 사용하는 클래스를 익명 클래스로 사용한다.
  • 생성자를 작성할 수 없다.
  • 익명객체 내부에서 정의한 맴버는 클래스 내부에서만 사용 가능하고 외부에서는 접근할 수 없다.

예제코드

    package anonymousclass;

    public class Member {
        int id;
        String name;
        
        public void setId(int id) {
            this.id = id;
        }

        public void setName(String name) {
            this.name = name;
        }

        public void printMember() {
            System.out.println("Member");
        }
    }
    package anonymousclass;

    public class TestAnonymousClass {
        public static void main(String[] args) {
            Member anonymousMember = new Member() {
                public void printClass() {
                    System.out.println("Member");
                }
                @Override
                public void printMember() {
                    printClass();
                    System.out.println(id + " : " + name);
                }
            };
            anonymousMember.setId(123);
            anonymousMember.setName("test");
            // Cannot resolve method 'printClass' in 'Member'
            // 익명클래스 내부에서 생성한 프로퍼티, 메소드는 익명객체 내부에서만 사용되고
            // 외부에서는 접근할 수 없다. -> Member형에는 printClass()가 정의되어있지 않기 때문
            //anonymousMember.printClass();
            anonymousMember.printMember();
        }
    }

1.4의 지역 내부클래스 예제코드2 변환

    // 지역 내부 클래스

    SquareInterface localTest() {
        class LocalClass implements SquareInterface {

            @Override
            public int square(int n) {
                return n * n;
            }
        }
        return new LocalClass();
    }
    // 익명 내부 클래스

    SquareInterface localTest() {
        return new SquareInterface() {

            @Override
            public int square(int n) {
                return n * n;
            }
        };
    }
    // 람다식

    SquareInterface localTest() {
        return n -> n * n;
    }

사용하는 이유 :

  1. 한번만 쓰고 말 클래스인데, 별도의 클래스로 만들어서 사용해버리면 클래스를 메모리에 올릴때 즉, 애플리케이션을 시작할 때 많은 시간이 소요된다. → 메소드 매개변수 대입할때도 사용 가능하므로, 이때 별도의 클래스를 선언하지 않아도 되는건 큰 장점이다.
	public static void main(String[] args) {
		Member member = new Member();
		Member.getPrinter(new Printer() {

			@Override
			public void printAnything() {
				System.out.println("Anything");				
			}
		});
	}
  1. 클로저(closure)라는 기능때문에 사용하기도 한다고 한다. 익명객체는 자기를 선언한 메소드의 변수에 접근 가능하기 때문에, 그 값을 매개변수로 받지 않고, 바로 사용할수 있는 장점이 있다고 한다.
    참고 : 명월일지
	public static void main(String[] args) {
		int ft = 42;
		Member member = new Member();
		Member.getPrinter(new Printer() {

			public void printAnything() {
  			// ft를 매개변수로 받지 않고 바로 사용.
				System.out.println(ft);				
			}
		});
	}

  • 중첩 클래스들은 외부클래스$중첩클래스.class형태로 별도의 클래스 파일이 생성된다.

참고 한 곳 : de_sja_wa.velog, viivii블로그, 작은발자국들을위한위대한여정


2. 람다식

2.1 함수형 프로그래밍과 람다식

  • 함수형 프로그래밍이란?

    함수형 프로그래밍은 순수 함수를 기반으로하는 프로그래밍이다. 순수 함수란 함수 외부의 변수를 사용하지 않고, 함수 내부의 지역변수와 매개변수만을 활용하여 구현한 함수다. 함수 외부 변수를 사용하지 않기 때문에 함수 외부에 영향을 주지 않아 안정적이다.

  • 자바8 이전 (함수형 프로그래밍 도입 전)

    필요한 기능이 있으면 클래스를 만든 후, 그 안에 메서드를 구현해야 호출하고 사용할 수 있었다.

  • 자바8 이후

    자바 8부터 함수형 프로그래밍을 지원하면서, 클래스 없이 메서드를 구현하고 사용하는 것이 가능해졌다. 자바의 함수 프로그래밍 방식을 람다식이라고 한다.

2.2 람다식 구현하기

  • 일반 메서드 형식
[반환 자료형] [함수명](매개변수...) {
		[구현부]
}
  • 람다식
(매개변수...) -> {구현부}

// 함수명과 반환자료형 생략

  • 예제코드
// 변환 전
void printHelloWorld() {
	System.out.println("Hello");
	System.out.println("World");
}

// 람다식으로 변환
() -> {
	System.out.println("Hello");
	System.out.println("World");
}
// 변환 전
int add(int x, int y) {
	return x + y;
}

// 람다식으로 변환
(int x, int y) -> {return x + y;}
// 변환 전
void printStr(String s)
	System.out.println(s);
}

// 람다식으로 변환
(String s) -> {System.out.println(s);}

2.3 람다식 문법 및 활용

람다식을 구현할 때는 되도록 생략할 수 있는 부분은 생략하여 구현한다.

  1. 매개변수 자료형 생략
// 생략 전
(int x, int y) -> {return x + y;}
(String s) -> {System.out.println(s);}

// 생략 후
(x, y) -> {return x + y;}
(s) -> {System.out.println(s);}
  1. 매개변수가 하나일 경우 매개변수를 감싸는 괄호 생략가능
    (매개변수가 하나도 없거나 두개 이상이면 괄호는 생략 불가능 하다.)
// 생략 전
(s) -> {System.out.println(s);}

// 생략 후
s -> {System.out.println(s);}
  1. 중괄호 안의 구현부가 한 줄이면 중괄호 생략 가능
    (그러나 return 예약어가 있으면 중괄호 생략이 불가하므로 중괄호를 생략하고 싶다면 return 예약어도 생략해야한다.)
// 생략 전
s -> {System.out.println(s);}
(x, y) -> {return x + y;}

// 생략 후
s -> System.out.println(s);
(x, y) -> x + y

2.4 함수형 인터페이스

람다식은 클래스 없이 구현되는 익명 함수이다. 그런데 자바는 참조변수 없이 메서드를 호출하는 것이 불가능하며, 변수는 클래스 바깥에서는 사용이 불가능하다. 그렇다면 람다식은 어디서 선언하고 구현해야할까?

그래서 람다식을 사용하기 위해서는 함수형 인터페이스가 필요하다. 람다식으로 구현할 함수를 인터페이스 안에 선언해두고, 람다식을 인터페이스 변수에 대입하여 사용하는 것이다.

  • 함수형 인터페이스
package lamdaex;

public interface LamdaInterface {
	int square(int n);
}
  • 람다식 인터페이스 변수에 대입하고 사용하기
package lamdaex;

public class LamdaTest {
	public static void main(String[] args) {
		LamdaInterface squareNumber = n -> n * n;   // 인터페이스 변수에 대입
		System.out.println(squareNumber.square(5));
	}
}
25

람다식은 익명 함수이기 때문에 함수형 인터페이스에 메서드가 여러개 선언되어있다면 모호해진다. 따라서 람다식을 사용하기 위해 만든 함수형 인터페이스에는 메서드가 하나만 선언 되어있어야하고, 혹시 모를 실수에 대비하고 싶다면, 애노테이션을 이용하면 된다.

package lamdaex;

@FunctionalInterface
public interface LamdaInterface {
    int square(int n);
}

@FunctionalInterface 은 함수형 인터페이스 안에 메서드를 2개 이상 선언되어있다면 오류로 처리한다.

2.5 객체지향 프로그래밍 방식과 람다식 비교

  • (바로 위 예제코드) 객체지향 프로그래밍 방식으로 변환해보기
    // 인터페이스
    
    package lamdaex;

    public interface LamdaInterface {
        int square(int n);
    }
    // 인터페이스를 구현한 클래스
    package lamdaex;

    public class LamdaClass implements LamdaInterface{
        @Override
        public int square(int n) {
            return n * n;
        }
    }
    // 출력 Test
    package lamdaex;

    public class LamdaTest {
        public static void main(String[] args) {
            LamdaClass squareNumber = new LamdaClass();
            System.out.println(squareNumber.square(6));
        }
    }
    36
  • 익명 클래스를 사용하여 변환해보기
    // 인터페이스
    package lamdaex;

    public interface LamdaInterface {
        int square(int n);
    }
    // 익명 클래스를 활용하여 구현
    package lamdaex;

    public class LamdaTest {
        public static void main(String[] args) {
            LamdaInterface squareNumber = new LamdaInterface() {
                @Override
                public int square(int n) {
                    return n * n;
                }
            };
            System.out.println(squareNumber.square(5));
        }
    }
    25

람다식으로 구현할 때와 다르게 인터페이스를 구현한 클래스를 정의하는 단계를 한번 더 거쳐야하는 번거로움이 있다. 그에 비해 람다식은 클래스 없이 함수 구현이 가능하므로 코드가 간결하다.

2.6 익명 객체를 생성하는 람다식

객체 지향 언어인 자바에서 객체의 생성없이 함수를 사용할 수 있다는 개념은 생소하게 느껴진다. 그러나 코드 상에서만 객체의 생성이 명시적으로 나타나지 않을 뿐, 람다식을 사용할 때도 컴퓨터 내부에서는 객체의 생성이 일어난다. 람다식을 사용할 경우 다음과 같은 변환과정이 일어난다.

  • 람다식을 사용하는 코드
package lamdaex;

public class LamdaTest {
    public static void main(String[] args) {
        LamdaInterface squareNumber = n -> n * n;
        System.out.println(squareNumber.square(5));
    }
}
  • 컴퓨터 내부
package lamdaex;

public class LamdaTest {
    public static void main(String[] args) {
        LamdaInterface squareNumber = new LamdaInterface() {
            @Override
            public int square(int n) {
                return n * n;
            }
        };
        System.out.println(squareNumber.square(5));
    }
}

람다식으로 구현한 메서드가 호출되는 순간 위와 같이 익명 클래스가 생성되고, 객체가 만들어진다. 그래서 클래스 없이 구현한 람다식도 사용할 수 있는 것이다. 람다식으로 함수를 구현하게 되면 익명 내부 클래스를 사용하여 구현한 것과 마찬가지다. 따라서 람다식은 익명 내부 클래스와 마찬가지로 메서드 외부의 변수를 변경할 수 없다. (사용은 가능)
익명 내부 클래스는 함수 내에서 만들어진 클래스 이므로, 클래스를 감싸고 있는 함수가 스택 메모리에서 소멸되는 순간 함께 사라진다. 그러므로 익명 내부 클래스와 람다식 내부에서는 함수 외부에서 가져온 변수의 값을 변경할 수 없으며, 외부 변수는 컴파일 과정에서 final 변수, 즉 상수로 변경된다.

2.7 함수를 변수처럼 사용하는 람다식

람다식을 사용할 때, 함수형 인터페이스 자료형 변수에 람다식을 대입하여 사용하였다. 이처럼 람다식은 함수지만 변수처럼 사용할 수 있는데, 변수에 대입하는 것 말고도 람다식을 매개변수로 전달하거나, 반환값으로 받을 수도 있다.

  1. 변수에 대입
    package lamdaex;

    public class LamdaTest {
        public static void main(String[] args) {
            LamdaInterface squareNumber = n -> n * n;
            System.out.println(squareNumber.square(5));
        }
    }
  1. 매개변수로 전달
    package lamdaex;

    public class LamdaTest {

        public static void printReturnValue(LamdaInterface lamdaInterface, int n) {
            System.out.println(lamdaInterface.square(n));
        }

        public static void main(String[] args) {
            LamdaInterface squareNumber = n -> n * n;
            printReturnValue(squareNumber, 10);
        }
    }
    10

람다식을 대입한 인터페이스 자료형을 매개변수로 하면, 람다식을 매개변수를 통해 전달 가능하다.

  1. 반환값으로 사용
    package lamdaex;

    public class LamdaTest {

        public static LamdaInterface returnLamda() {
            return n -> n * n;
        }

        public static void main(String[] args) {
            LamdaInterface squareNumber = returnLamda();
            System.out.println(squareNumber.square(7));
        }
    }
    49

매개변수로 전달할 때와 마찬가지로 반환형을 함수형 인터페이스로 하면 람다식을 반환시킬 수 있다.


3. 스트림

3.1 스트림이란?

자바 8부터 추가된, 컬렉션의 각 저장요소를 람다식으로 처리할 수 있도록 해주는 반복자. Iterator과 비슷하지만 람다식을 사용할 수 있으며, 내부 반복자를 통해 반복문처리(forEach() 사용) 및 병렬처리가 쉽다(parallelStream 사용).

  • 자료의 대상과 관계없이 동일한 연산을 수행
  1. 배열, 컬렉션을 대상으로 동일한 연산을 수행함
  2. 일관성 있는 연산으로 자료의 처리를 쉽고 간단하게 함
  • 한번 생성하고 사용한 스트림은 재사용 할 수 없음
  1. 자료에 대한 스트림을 생성하여 연산을 수행하면 스트림은 소모됨
  2. 다른 연산을 위해서는 새로운 스트림을 생성함
  • 스트림 연산은 기존 자료를 변경하지 않음
  1. 자료에 대한 스트림을 생성하면 별도의 메모리 공간을 사용하므로 기존 자료를 변경하지 않음
  • 스트림 연산은 중간 연산과 최종 연산으로 구분됨
  1. 스트림에 대해 중간 연산은 여러개 적용될 수 있지만 최종 연산은 마지막에 한 번만 적용됨
  2. 최종 연산이 호출되어야 중간 연산의 결과가 모두 적용됨 , 이를 '지연 연산' 이라 함

3.2 스트림 생성 및 사용

package javaInnerClass;
import java.util.Arrays;
import java.util.stream.IntStream;

public class Stream {
    public static void main(String[]args)
    {
        int[] arr = {1,2,3,4,5};
        IntStream stream = Arrays.stream(arr);
        int sum = stream.sum();
        System.out.println(sum);

        System.out.println("new =============");
        int newCount = (int)stream.count();
        System.out.println(newCount);
        /*int count = (int)Arrays.stream(arr).count();
        System.out.println(count);*/
    }
}
15
new =============
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
	at java.base/java.util.stream.IntPipeline.count(IntPipeline.java:488)
	at javaInnerClass.Stream.main(Stream.java:14)

ERROR 발생

  • 이미 생성된 stream 을 다시 재사용 할 수 없음

새로운 stream 생성

package javaInnerClass;
import java.util.Arrays;
import java.util.stream.IntStream;

public classStream{
    public static void main(String[]args)
    {
        int[] arr = {1,2,3,4,5};
				IntStream stream =Arrays.stream(arr);
        int sum = stream.sum();
				System.out.println(sum);

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

		    int count = (int)Arrays.stream(arr).count();     //stream 새로 생성
				System.out.println(count);
    }
}
15
new =============
5

3.3 ArrayList를 사용한 stream 연산

import java.util.List;
import java.util.stream.Stream;

public classStreaArrayList{
    public static void main(String[]args)
    {
				List<String> sList = new ArrayList<String>();
        sList.add("james");
        sList.add("tames");
        sList.add("tomas");

				Stream<String> stream = sList.stream();
        stream.forEach(s->System.out.println(s));
				/*
        for (String s : sList)
        {
            System.out.println(s);
        }*/
				System.out.println("=====================");
				sList.stream().sorted().forEach(s->System.out.println(s));
					//stream 을 사용해서 바로 출력하기
		}
}
james
tames
tomas
=====================
james
tames
tomas

3.4 reduce()연산

  • 정의된 연산이 아닌 프로그래머가 직접 지정하는 연산을 적용
  • 최종 연산으로 스트림의 요소를 소모하며 연산을 수행
  • 배열의 모든 요소의 합을 구하는 reduce()연산
	Arrays.stream(arr).reduce(0,(a, b)->(a+b));
  • 두 번째 요소로 전달 되는 람다식에 따라 다양한 기능을 수행

람다식을 이용한 reduce 함수 사용

package javaInnerClass;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class ReduceTest {
    public static void main(String []args)
    {
        String[] strs = {"안녕하세요", "저는", "진성대", "입니다"};
				System.out.println("lambda 식으로 구현 =============");
        System.out.println(Arrays.stream(strs).reduce("",(s1,s2)->{
            if (s1.getBytes().length >= s2.getBytes().length)
                return s1;
            else
                return s2;
        }));
    }
}
lambda 식으로 구현 =============
안녕하세요

Class 를 이용한 reduce 사용

package javaInnerClass;

import java.util.function.BinaryOperator;
import java.util.Arrays;
class CompareString implements BinaryOperator<String>
{
    @Override
    public String apply(String t, String u)
    {
        if (t.getBytes().length >= u.getBytes().length)
            return t;
        else
            return u;
    }
}
public class ReduceTest2 {
    public static void main(String[]args)
    {
        String[] strs2 = {"hellow world", "GoodMorning", "hi EveryOne Two Three"};
        String str = Arrays.stream(strs2).reduce(new CompareString()).get();
        System.out.println(str);
    }
}
hi EveryOne Two Three

0개의 댓글