본문 바로가기

Java-Study

Java Study : 9주차 정리 - 내부 클래스, 람다식, 스트림

내부 클래스

  • 내부 클래스는 클래스 내부에 선언한 클래스이다.
  • 내부 클래스는 선언하는 위치나 예약어에 따라 크게 네 가지 유형으로 나누어진다.
    • 인스턴스 내부 클래스, 정적 내부 클래스, 지역 내부 클래스는 클래스 내부의 선언하는 변수의 유형과 유사하다.
    • 익명 내부 클래스는 클래스 이름 없이 선언하고 바로 사용이 가능하다.

인스턴스 내부 클래스

  • 인스턴스 변수를 선언할 때와 같은 위치에 선언하며, 외부 클래스 내부에서만 생성하여 사용하는 객체를
    선언할 때 쓴다.
  • 인스턴스 내부 클래스는 외부 클래스가 먼저 생성되어야 생성 가능하다.
package innerclass;

class OutClass {    //외부 클래스
    private int num = 10;   //외부 클래스 private 변수
    private static int sNum = 20;   //외부 클래스 정적 변수
    private InClass inClass;    //내부 클래스 자료형 변수를 먼저 선언

    public OutClass(){  //외부 클래스 디폴트 생성자
        inClass = new InClass(); // 내부 클래스 생성
    }

    class InClass{  //인스턴스 내부 클래스

        int inNum = 100;
        //static int sInNum = 200;  //인스턴스 내부 클래스에 정적 변수 선언 불가능

        void inTest(){
            System.out.println("OutClass num = " +num + "(외부 클래스의 인스턴스 변수)");
            System.out.println("OutClass sNum = " + sNum + "(외부 클래스의 스태틱 변수)");
        }

        //static void sTest(){  //정적 메서드 역시 정의 불가능

        //}

    }

    public void usingClass(){
        inClass.inTest(); //내부 클래스 변수를 사용하여 메서드 호출하기
    }



public class InnerTest{

    public static void main(String[] args){

        OutClass outClass = new OutClass();
        System.out.println("외부 클래스 이용하여 내부 클래스 기능 호출");
        outClass.usingClass();    // 내부 클래스 기능 호출
    }
}

 

정적 내부 클래스

  • 내부 클래스가 외부 클래스 생성과 무관하게 사용할수 있어야 하고 정적 변수도 사용할 수 있어야 할 때 사용한다.
  • static 예약어를 함께 사용한다.
  • 정적 내부 클래스는 외부 클래스를 생성하지 않고도 내부 클래스 자료형으로 바로 선언하여 생성할 수 있다.
package innerclass;

class OutClass {    //외부 클래스
    private int num = 10;   //외부 클래스 private 변수
    private static int sNum = 20;   //외부 클래스 정적 변수


    static class InStaticClass{	//정적 내부 클래스
	
        int inNum = 100;	//정적 내부 클래스의 인스턴스 변수
        static int sInNum = 200;	//정적 내부 클래스의 정적 변수

        void inTest(){   //정적 클래스의 일반 메서드
            //num += 10;    // 외부 클래스의 인스턴스 변수는 사용할 수 없음.
            System.out.println("InStaticClass inNum = " + inNum + "(내부 클래스의 인스턴스 변수 사용)");
            System.out.println("InStaticClass sInNum = " + sInNum + "(내부 클래스의 스태틱 변수 사용)");
            System.out.println("OutClass sNum = " + sNum + "(외부 클래스의 스태틱 변수 사용)");
        }

        static void sTest(){  // 정적 내부 클래스의 static 메서드
            //num += 10;   // 외부 클래스의 인스턴스 변수는 사용할 수 없음.
            //inNum += 10; // 내부 클래스의 인스턴스 변수는 사용할 수 없음

            System.out.println("OutClass sNum = " + sNum + "(외부 클래스의 스태틱 변수 사용)");
            System.out.println("InStaticClass sInNum = " + sInNum + "(내부 클래스의 스태틱 변수 사용)");

        }
    }
}


public class InnerTest{

    public static void main(String[] args){

		OutClass.InClass inClass = outClass.new InClass();   // 외부 클래스를 이용하여 내부 클래스 생성
		System.out.println("외부 클래스 변수를 이용하여 내부 클래스 생성");
		inClass.inTest();

		System.out.println();

		//외부 클래스 생성하지 않고 바로 정적 내부 클래스 생성
		OutClass.InStaticClass sInClass = new OutClass.InStaticClass();
		System.out.println("정적 내부 클래스 일반 메서드 호출");
		sInClass.inTest();
		System.out.println();

		System.out.println("정적 내부 클래스의 스태틱 메소드 호출");
		OutClass.InStaticClass.sTest();

    }
}

 

지역 내부 클래스

  • 지역 변수처럼 메서드 내부에 클래스를 정의하여 사용하는 것을 말한다. -> 메서드 안에서만 사용 가능
package innerclass;


class Outer{

    int outNum = 100;
    static int sNum = 200;


    Runnable getRunnable(int i){

        int num = 100;  //지역 변수

        class MyRunnable implements Runnable{   //지역 내부 클래스

            int localNum = 10;  //지역 내부 클래스 인스턴스 변수

            @Override
            public void run() {
                //num = 200;   //지역 변수는 상수로 바뀌므로 값을 변경할 수 없어 오류 발생
                //i = 100;     //매개변수도 지역 변수처럼 상수로 바뀌므로 값을 변경할 수 없어 오류 발생
                System.out.println("i =" + i);
                System.out.println("num = " +num);
                System.out.println("localNum = " +localNum);

                System.out.println("outNum = " + outNum + "(외부 클래스 인스턴스 변수)");
                System.out.println("Outter.sNum = " + Outer.sNum + "(외부 클래스 정적 변수)");
            }
        }
        return new MyRunnable();
    }
}

public class LocalInnerTest {

    public static void main(String[] args) {

        Outer out = new Outer();
        Runnable runner = out.getRunnable(10);  //메서드 호출
        runner.run();
    }
}

 

익명 내부 클래스

  • 클래스 이름을 사용하지 않는 클래스이다.
package innerclass;

class Outter2{

    Runnable getRunnable(int i){

        int num = 100;
        
        //MyRunnable 클래스 이름을 빼고 클래스를 바로 생성하는 방법
        return new Runnable() { //익명 내부 클래스, Runnable 인터페이스 생성

            @Override
            public void run() {
                //num = 200;   //에러 남
                //i = 10;      //에러 남
                System.out.println(i);
                System.out.println(num);
            }
        };  //클래스 끝에 ;를 붙임
    }
    
    //인터페이스나 추상 클래스형 변수를 선언하고 클래스를 생성해 대입하는 방법
    Runnable runner = new Runnable() {  //익명 내부 클래스를 변수에 대입

        @Override
        public void run() {
            System.out.println("Runnable 이 구현된 익명 클래스 변수");

        }
    };
}

public class AnonymousInnerTest {

    public static void main(String[] args) {
        Outter2 out = new Outter2();

        Runnable runnerble = out.getRunnable(10);
        runnerble.run();

        out.runner.run();
    }
}

 

람다식

  • 프로그래밍 언어 중에는 함수의 구현과 호출만으로 프로그램을 만들 수 있는 프로그래밍 방식이 있는데 이를
    함수형 프로그래밍이라 하고, 자바에서 제공하는 함수형 프로그래밍을 람다식이라고 한다.
  • 람다식은 함수 이름이 없는 익명 함수를 만든다고 생각하면 된다.
    • (매개변수) -> { 실행문; }

 

문법 살펴보기

  • 람다식 문법에서는 매개변수 자료형을 생략할 수도 있고, 하나인 경우에는 괄호도 생략 가능하다.
    • str -> {System.out.println(str);}
  • 중괄호 안의 구현 부분이 한 문장인 경우 중괄호를 생략할 수 있다.
    • str -> System.out.println(str);
    • 하지만 return문의 경우 한 문장이더라도 생략 불가능 하다.
  • 중괄호 안의 구현 부분이 return문 하나라면 중괄호와 return 모두 생략하고 식만 쓸 수 있다.
    • (x, y) -> x + y
      str -> str.length()

 

람다식 사용하기

  • 함수형 인터페이스 선언하기
package lambda;

@FunctionalInterface
public interface MyNumber {
	
	int getMax(int num1, int num2);	//추상 메서드 선언
}

 

  • 람다식 구현과 호출
package lambda;

public class TestMyNumber {

    public static void main(String[] args) {
        MyNumber max = (x, y)->(x>= y)? x:y; // 람다식을 인터페이스 자료형 max 변수에 대입

        System.out.println(max.getMax(10, 20));// 인터페이스 자료형 변수로 함수 호출

        //	MyNumber max2 = (x, y)-> x>=y ?x:y;
        //	System.out.println(max2.getMax(10, 20));
    }
}

 

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

  • 인터페이스 구현하기
package lambda;

public interface StringConcat {
	
	public void makeString(String s1, String s2);

}

 

클래스에서 인터페이스 구현하기

  • 추상 메서드 구현하기
package lambda;

public class StringConCatImpl implements StringConcat{

    @Override
    public void makeString(String s1, String s2) {
        System.out.println( s1 + "," + s2 );
    }
}
  • 메서드 테스트하기
package lambda;

public class TestStringConcat {

    public static void main(String[] args) {

        String s1 = "Hello";
        String s2 = "World";
        StringConCatImpl concat1 = new StringConCatImpl();
        concat1.makeString(s1, s2);
}

makeString() 메서드를 수행하려면 StringConcat 인터페이스를 구현한 StringConCatImpl 클래스를 인스턴스로

생성해야 한다.

 

람다식으로 인터페이스 구현하기

  • 람다식으로 인터페이스 구현하기
package lambda;

public class TestStringConcat {

    public static void main(String[] args) {

        String s1 = "Hello";
        String s2 = "World";

        StringConcat concat2 = (s, v)->System.out.println(s + "," + v ); //System.out.println(i);
        concat2.makeString(s1, s2);


}

두 매개변수 s, v를 사용해 연결된 문자열을 출력하도록 구현하였는데, 이 부분은 concat2에 대입하고 변수를 사용하여

makeString() 메서드를 호출했다. 람다식으로 구현하는 경우 코드가 더 간결해 지는 것을 알 수 있다.

 

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

  • 람다식으로 메서드를 구현해서 호출하면 컴퓨터 내부에서는 익명 클래스가 생성되고 이를 통해 익명 객체가
    생성되게 되는 것이다.
  • 람다식을 사용하면 익명 내부 클래스가 생성되기 때문에 람다식 내부에서 변수를 변경하면 오류가 발생한다.

 

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

  • 람다식을 이용하면 구현된 함수를 변수처럼 사용할 수 있다.

 

인스턴스형 변수에 람다식 대입하기

  • 함수형 인터페이스에 메서드를 하나 선언하고 이 메서드를 람다식으로 구현한다.
  • 실행하기 위해 인터페이스형 변수를 선언하고 람다식 구현부를 대입
interface PrintString {
     void showString(String str);
}

PrintString lambdaStr = s -> System.out.println(s);
lambdaStr.showString("Hello Lambda");

 

매개변수로 전달하는 람다식

  • 람다식을 변수에 대입하면 이를 매개변수로 전달할 수 있다. 이 때 전달되는 매개변수는 인터페이스형이다.
package lambda;

interface PrintString{

    void showString(String str);
}

public class TestLambda {

    public static void main(String[] args) {
        
        PrintString lambdaStr = s->System.out.println(s);   //람다식을 인터페이스형 변수에 대입, 그 변수를 사용해 람다식 구현부 호출
        lambdaStr.showString("hello lambda_1");

        showMyString(lambdaStr);                          //메서드의 매개변수로 람다식을 대입한 변수 전달

    }

    public static void showMyString(PrintString p) {    //매개변수를 인터페이스형으로 받음
        p.showString("hello lambda_2");
    }
}

 

반환 값으로 쓰이는 람다식

  • 메서드의 반환형을 람다식의 인터페이스형으로 선언하면 구현한 람다식을 반환할 수 있다.
package lambda;

interface PrintString{

    void showString(String str);
}

public class TestLambda {

    public static void main(String[] args) {
        
        PrintString lambdaStr = s->System.out.println(s);   //람다식을 인터페이스형 변수에 대입, 그 변수를 사용해 람다식 구현부 호출
        lambdaStr.showString("hello lambda_1");

        showMyString(lambdaStr);                          //메서드의 매개변수로 람다식을 대입한 변수 전달

        PrintString reStr = returnString();     //변수로 반환받기
        reStr.showString("hello ");        //메서드 호출

    }

    public static void showMyString(PrintString p) {    //매개변수를 인터페이스형으로 받음
        p.showString("hello lambda_2");
    }

    public static PrintString returnString() {         //반환 값으로 사용
        return s->System.out.println(s + "world");  //람다식을 반환하는 메서드
    }
}

 

 

스트림

  • 여러 자료의 처리에 대한 기능을 구현해 놓은 클래스를 스트림이라 한다.
  • 스트림을 활용하면 배열, 컬렉션 등의 자료를 일관서  있게 처리할 수 있다.

 

스트림 연산

  • 크게 중간 연산과 최종 연산 두 가지가 있다.
  • 중간 연산은 자료를 거르거나 변경하여 또 다른 자료를 내부적으로 생성한다.
    • filter() : 조건을 넣고 그 조건에 맞는 참인 경우만 추출하는 경우에 사용
    • map() : 클래스가 가진 자료 중 이름만 출력하는 겨웅에 사용
  • 최종 연산은 생성된 내부 자료를 소모해 가면서 연산을 수행하기 때문에 마지막에 한 번만 호출된다.
    • forEach() : 요소를 하나씩 꺼내는 기능을 수행
    • sum(), count() : 배열 요소의 합계나 개수를 출력하는 연산을 수행

 

스트림 생성하고 사용하기

  • 정수 배열에 스트림 생성하고 사용하기
package stream;

import java.util.Arrays;

public class IntArrayTest {

    public static void main(String[] args) {
        int[] arr = {1,2,3,4,5};

        int sumVal = Arrays.stream(arr).sum();  //sum() 연산으로 arr 배열에 저장된 값을 모두 더함
        int count = (int) Arrays.stream(arr).count();   //count() 연산으로 arr 배열의 요소 개수를 반환함
        
        System.out.println(sumVal);
        System.out.println(count);

    }
}

 

  • ArrayList에서 스트림 활용하기
package stream;

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

public class ArrayListStreamTest {

    public static void main(String[] args) {

        List<String> sList = new ArrayList<String>();
        sList.add("Tomas");
        sList.add("Edward");
        sList.add("Jack");

        Stream<String> stream = sList.stream();     //스트림 생성
        stream.forEach(s->System.out.print(s + " "));   //배열의 요소를 하나씩 출력
        System.out.println();

        sList.stream().sorted().forEach(s->System.out.print(s+ " "));
        //스트림 새로 생성.정렬.요소를 하나씩 꺼내어 출력

    }
}

 

스트림의 특징

  • 자료의 대상과 관계없이 동일한 연산을 수행한다.
  • 한 번 생성하고 사용한 스트림은 재사용할 수 없다.
  • 스트림의 연산은 기존 자료를 변경하지 않는다.
  • 스트림의 연산은 중간 연산과 최종 연산이 있다.

 

프로그래머가 기능을 지정하는 reduce() 연산

  • reduce() 연산은 내부적으로 스트림의 요소를 하나씩 소모하면서 프로그래머가 직접 지정한 기능을 수행한다.
package stream;

import java.util.Arrays;
import java.util.function.BinaryOperator;

class CompareString implements BinaryOperator<String>{  //BinaryOperator를 구현한 클래스 정의

    @Override
    public String apply(String s1, String s2) {     //reduce() 메서드가 호출될 때 불리는 메서드
                                                    //두 문자열 길이를 비교
        if (s1.getBytes().length >= s2.getBytes().length) return s1;
        else return s2;
    }
}

public class ReduceTest {

    public static void main(String[] args) {

        String[] greetings = {"안녕하세요~~~", "hello", "Good morning", "반갑습니다^^"};
        
        //람다식을 직접 구현하는 방법
        System.out.println(Arrays.stream(greetings).reduce("", (s1, s2)->
        {if (s1.getBytes().length >= s2.getBytes().length)
            return s1;
        else return s2;}));

        String str = Arrays.stream(greetings).reduce(new CompareString()).get(); //BinaryOperator를 구현한 클래스 이용   
        System.out.println(str);

    }
}