4주차 ( 람다식 , 입출력 )

2019. 5. 11. 15:14Java

[ ※ 실습예제로 쓰인 소스는 자바의 정석 개정판 & 구글 검색을 참고하였습니다. ]

 

  • 람다식(Lambda expression)이 란?


- 람다식은 간단히 말해서 메서드를 하나의 '식(expression)'으로 표현한 것이다. 람다식은 함수를 간략하면서도 명확한 식으로 표현할 수 있게 해준다.
  메서드를 람다식으로 표현하면 메서드의 이름과 반환값이 없어지므로, 람다식을 '익명 함수(anonymous function)'이라고도 한다.

 
- 모든 메서드들은 클래스에 포함되어야 하므로 클래스도 새로 만들어야 하고, 객체도 생성해야만 비로소 이 메서드를 호출할 수 있다.
  하지만, 람다식은 이 모든 과정없이 오직 람다식 자체만으로도 이 메서드의 역할을 대신할 수 있다.

 

다음 아래  특징 & 장단점들을 살펴보자. 

특징

          (1) 익명 - 보통의 메서드와 달리 이름이 없으므로 익명이다.

          (2) 함수 - 람다는 메서드처럼 특정 클래스에 종속되지 않으므로 함수 라고할 수 있다. 

          (3) 전달 - 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.

장점

  1.  코드를 간결하게 구성할 수 있다 : 5줄의 코드를 1줄로도 표현이 가능하다.

  2.  병렬 프로그래밍이 가능하다 : iteration방식은 반복대상을 일일히 루프에서 지정하는 반면에, 함수형 프로그래밍에서는 반복대상을 사용자코드에서 직접 지정하지 않는다. 
    (이로 인해 Collection API가 크게 효과적으로 개선되었다.)
    (*ITERATION:그대로 반복이다. 특정 횟수만큼 또는 어떤 조건이 만족될 때까지 명령을 반복하는 것을 뜻하며, 명시적으로든 암묵적으로든 반복문을 사용해 객체의 여러 원소에 하나하나 접근하는것을 말한다.)

  3. 메소드로 행동방식을 전달할수 있다.
  4. 의도의 명확성

단점

  1.  람다식의 호출을 위해 직접 메소드를 불러야 한다. : 람다식을 생성하여 다른 함수의 파라미터로 전달하는 것은 자연스러우나, 람다식을 실행할때에는 인터페이스에 선언된 메소드를 호출하여야 한다.

  2.  재귀 람다식의 호출이 까다롭다. : 람다식안에서 자신을 다시 호출하기가 용이하지 않다. 람다식안에서는 람다식을 가리키는 변수를 참조할수가 없다. 배열등의 트릭을 사용하면 가능하기는 하다.

  3. 클로저가 지원되지 않는다. 람다식안에서 자신을 다시 호출하기가 용이하지 않다. 람다식안에서는 람다식을 가리키는 변수를 참조할수가 없다. (배열등의 트릭을 사용하면 가능 하지만 불편하다.)

  4. 함수 외부의 값을 변경한다.

 

 

문법

(매개변수목록) -> { 함수몸체 }
 -  (parameter) -> body
 -  (parameter) -> { body }
 -  () -> body
 -  () -> { body } 

사용시 주의사항

   1) 매개변수의 타입을 추론할 수 있는 경우에는 타입을 생략할 수 있다.

   2) 매개변수가 하나인 경우에는 괄호(())를 생략할 수 있다.

   3) 함수의 몸체가 하나의 명령문만으로 이루어진 경우에는 중괄호({})를 생략할 수 있다. (이때 세미콜론(;)은 붙이지 않음)

   4) 함수의 몸체가 하나의 return 문으로만 이루어진 경우에는 중괄호({})를 생략할 수 없다.

   5) return 문 대신 표현식을 사용할 수 있으며, 이때 반환값은 표현식의 결과값이 된다. (이때 세미콜론(;)은 붙이지 않음)

 

 

일반 메서드

RamdaEx01.java

public void Print(ActionEvent actionevent) {
 
  System.out.println("Hello");
 
}

 

RamdaEx02.java

public int Sum(int a, int b) {
 
   return a+b;
 
}

 

람다식

RamdaEx01_1.java

actionevent -> System.out.println("Hello");

RamdaEx02_1.java

sum = (a,b) -> a+b;

 

 

 

  • 람다식 작성하기


- 반환값이 있는 매서드의 경우, return문 대신  '식(expression)'으로 대신 할 수 있다. 식의 연산결과가 자동적으로 반환값이 된다. 이때는 '문장(statement)'이 아닌 '식'이므로 끝에 ';' 을 붙이지 않는다.
 

RamdaEx03.java

(int a, int b) -> { return a > b ? a : b;}





(int a, int b) -> a > b ? a : b

 

- 람다식에 선언된 매개변수의 타입은 추론이 가능한 경우는 생략할 수 있는데, 대부분의 경우에 생략가능하다. 람다식에 반환타입이 없는 이유도 항상 추론이 가능하기 때문이다.
(단, (int a, b) 와 같이 두 매개변수 중 하나의 타입만 생략하는 것은 허용되지 않는다.)

 

RamdaEx03_2.java

(a) -> a * a                         // a     -> a * a           OK



(int a, b) -> a * b                      // int a , b -> a * b            에러

 

- 괄호 {}안의 문장이 하나일 때는 괄호{}를 생략할 수 있다. 이 때 문장의 끝에 ';'을 붙이지 않아야 한다는 것에 주의하자.

RamdaEx03_3.java

(String name, int i) -> {

    System.out.println(name+"="+i);

}   // 생략전





(String name, int i) -> 

    System.out.println(name+"="+i)

    // 생략후

 

- 그러나, 괄호 {}안의 문장이 return문일 경우 괄호{}를 생략할 수 없다.

 

RamdaEx03_4.java

(int a, int b) -> { return a > b ? a : b; }       // OK



(int a, int b) -> return a > b ? a : b            // 에러

 

 

  • 함수형인터페이스 (Functional Interface)


- 함수형인터페이스란? 추상메소드를 단 하나만 가지는 인터페이스를  지칭하는 말이다.

당장 생각나는 인터페이스중에는 Thread구현시 사용하는 Runnable 인터페이스가 있지만, 임의로 함수형 인터페이스를 하나 생성해서 예시로 들어보겠다.
아래처럼 FInterface라는 이름의 인터페이스를 하나 생성했다.

FInterface(인터페이스)는 aaa()라는 이름의 매개변수가 없고 리턴타입이 void인 추상메소드를 하나 가지고 있다.

이것이 기본적인 함수형 인터페이스의 예이다.

추가적으로, @FunctionalInterface라는 어노테이션을 통해 해당 인터페이스가 함수형 인터페이스임을 명시적으로 표현할 수 있다.
(@FunctionalInterface를 명시할 경우 두 개 이상의 추상메서드를 가질 수 없도록 컴파일 시기에 에러를 발생시킨다.)

 

RamdaEx05_1.java

interface FInterface {          //어노테이션 명시 X

    public void aaa();

}





@FunctionalInterface

interface FInterface {          //어노테이션 명시 O

    public void aaa();

}



//-----------------함수형인터페이스 구현의 잘못된 예-----------------



@FunctionalInterface

interface FInterface {          //두 개 이상의 추상 매서드를 가질경우 에러

    public void aaa();

    public void bbb();

}

 

간단하게 Collections(List) 정렬구현예제를 통해 람다식의 장점을 알아보자.

 

RamdaEx05_1.java

List <String> list = Arrays.asList("abc", "aaa", "bbb", "ddd", "aaa");



Collections.sort(list, new Compareator<String> () {

    public int compare (String s1, String s2) {

        return s2,compareTo(s1);

    }

});

RamdaEx05_1.java

List <String> list = Arrays.asList("abc", "aaa", "bbb", "ddd", "aaa");



Collections.sort(list, (s1, s2) -> s2.compareTo(s1));

 

 

또한 람다식의 특징중 하나로 

함수형 인터페이스 타입의 매개변수와 반환타입

함수형 인터페이스 Myfunction이 아래와 같이 정의되어 있을 때,

 

RamdaEx05_2.java

@FunctionalInterface

interface MyFunction {

    void myMethod();

}

메서드의 매개변수가 MyFunction타입이면, 이 메서드를 호출할 때 람다식을 참조하는 참조변수를 매개변수로 지정해야하고

 

 

RamdaEx05_3.java

void aMethod(MyFunction f) {    //매개변수의 타입이 함수형 인터페이스

    f.myMethod();

}



MyFunction f = () -> System.out.println("myMethod()"); // or aMethod(()->System.out.println("myMethod()"));   // 람다식을 매개변수로 지정

                                                    //위처럼 참조변수 없이 직접 람다식을 매개변수로 지정하는 것도 가능하다.

aMethod(f);

메서드의 반환타입이 함수형 인터페이스타입이라면, 이 함수형 인터페이스의 추상 메서드와 동등한 람다식을 가리키는 참조변수를 반환하거나 람다식을 직접 반환할 수 있다.

 

RamdaEx05_4.java

MyFunction myMethod() {

    MyFunction f = ()->{};

    return f;               // 이 두 줄을 한 줄로 줄이면, ruturn () -> {};

}

람다식을 참조변수로 다룰 수 있다는 것은 메서드를 통해 람다식을 주고받을 수 있다는 것을 의미한다. 즉, 변수처럼 메서드를 주고받는 것이 가능.

 

 

RamdaEx06_1.java

@FunctionalInterface

interface MyFunction {

    void run();                 // public abstract void run();

}



class RamdaEx06_1 {

    static void execute(MyFunction f) {             // 매개변수의 타입이 MyFunction인 메서드

        f.run();

    }



    static MyFunction getMyFunction() {             // 반환 타입이 MyFunction인 메서드

        MyFunction f = () -> System.out.println("f3.run()");

        return f;

    }



    public static void main(String[] args) {        

        // 람다식으로 MyFunction의 run()을 구현      



        MyFunction f1 = () -> System.out.println("f1.run()");



        MyFunction f2 = new MyFunction() {  // 익명클래스로 run()을 구현

            public void run() { //public을 반드시 붙여야 함

                System.out.println("f2.run()");

            }

        };



        MyFunction f3 = getMyFunction();



        f1.run();

        f2.run();

        f3.run();



        execute(f1);

        execute( () -> System.out.println("run()") );

    }

}

 

[ 실행결과 ]

 

f1.run()

f2.run()

f3.run()

f1.run()

run()

 

람다식의 타입과 형변환

- 함수형 인터페이스로 람다식을 참조할 수 있는 것일 뿐, 람다식의 타입이 함수형 인터페이스의 타입과 일치하는 것은 아니다.
- 람다식은 익명 객체이고 익명 객체는 타입이 없다.

  정확히는 타입은 있지만 컴파일러가 임의로 이름을 정하기 때문에 알 수 없는 것이다. 그래서 대입 연산자의 양변의 타

  입을 일치시키기 위해 아래와 같이 형변환이 필요하다.
*MyFunction은 'interface MyFunction  { void method();   } 와 같이 정의되었다고 가정.

 

RamdaEx07.java

@FunctionalInterface

interface MyFunction {

    void myMethod();                    // public abstract void myMethod();

}



class RamdaEx07 {



    public static void main(String[] args) {       

     

        MyFunction f = () -> {};     // MyFunction f = (MyFunction) (()->{});

        Object obj = (MyFunction) (()->{});  // Object타입으로 형변환이 생략됨

        String str = ((Object)(MyFunction) (()->{})).toString();



        System.out.println(f);

        System.out.println(obj);

        System.out.println(str);



//      System.out.println(() -> {});   // 에러. 람다식은 Object타입으로 형변환 안됨

        System.out.println((MyFunction)(()-> {}));

//      System.out.println((MyFunction)(()-> {}).toString());    // 에러

        System.out.println(((Object)(MyFunction)(()->{})).toString());

    }

}

 

[ 실행결과 ]

 

YoonDev.RamdaEx07$$Lambda$1/1826771953@7229724f

YoonDev.RamdaEx07$$Lambda$2/1705736037@4c873330

YoonDev.RamdaEx07$$Lambda$3/455659002@eed1f14

YoonDev.RamdaEx07$$Lambda$4/295530567@776ec8df

YoonDev.RamdaEx07$$Lambda$5/1324119927@3b07d329

 

실행결과를 보면, 컴파일러가 람다식의 타입을 어떤 형식으로 만들어내는지 알 수 있다.

일반적인 익명 객체라면, 객체의 타입이 '외부클래스이름$번호'와 같은 형식으로 타입이 결정되었을 텐데, 람다식의 타입은 '외부클래스이름$$Lambda$번호'와 같은 형식으로 되어 있는 것을 확인할 수 있다.

 

외부 변수를 참조하는 람다식

람다식도 익명 객체, 즉 익명 클래스의 인스턴스이므로 람다식에서 외부에 선언된 변수에 접근하는 규칙은 익명 클래스에서의 규칙과 동일하다.

RamdaEx08.java

@FunctionalInterface

interface MyFunction {

    void myMethod();                   

}





class Outer {

    int val = 10;       // Outer.this.val

     

    class Inner {

        int val = 20;   // this.val

         

        void method(int i) {    // void method(final int i) {

            int val = 30;   // final int = 30;

//          i = 10;         // 에러. 상수의 값을 변경할 수 없음.

            MyFunction f = () -> {

                System.out.println("        i : " + i);

                System.out.println("      val : " + val);

                System.out.println(" this.val : " + ++this.val);

                System.out.println("Outer.this.val : " + ++Outer.this.val);

            };



            f.myMethod();

        }

    }// Inner 클래스의 끝

}// Outer클래스의 끝





                 



class RamdaEx08 {



    public static void main(String[] args) {       

        Outer outer = new Outer();

        Outer.Inner inner = outer.new Inner();

        inner.method(100);

    }

}

 

[ 실행결과 ]

 

i : 100

val : 30

this.val : 21

Outer.this.val : 11

 

 

 위 예제는 람다식 내에서 외부에 선언된 변수에 접근하는 방법을 보여준다. 람다식 내에서 참조하는 지역변수는 final이 붙지 않았어도 상수로 간주된다. 람다식 내에서 지역변수 와 val을 참조하고 있으므로 

람다식 내에서나 다른 어느 곳에서도 이 변수들의 값을 변경하는 일은 허용되지 않는다.

반면에, Inner클래스와 Outer클래스의 인스턴스 변수인 this.val과 Outer.this.val은 상수로 간주되지 않으므로 값을 변경해도 된다.

 

 

 

 

자바에서의 입출력(I/O)이란?


입출력이란?

  • I/O란, input / output의 약자로 컴퓨터 내부 또는 외부의 장치와 프로그램간의 데이터를 주고받는 것을 의미 한다.

스트림이란?

  • 바에서 입출력과 같이 데이터를 주고 받는데 사용되는 연결통로를 의미.
  • 스트림은 하나의 스트림으로 입력과 출력을 동시에 처리할 수 없기 때문에, 입출력을 위해서는 두개의 스트림이 필요하다.
  • 바이트기반 스트림이다. (스트림은 바이트 단위로 데이터를 전송하며 입출력 대상에 따라 여러 종류의 스트림이 있다.)
  • 스트림은 데이터를 읽고 기록하는 중간역할을 한다.

    비유하자면!
  • 스트림은 빨대다.
  • 빨대는 음료수를 마시는 중간역할을 한다.
  • 빨대는 입에 있는 음료수를 다시 내뱉는 중간역할을 한다.
  • 스트림은 단 방향 빨대이다. 음료수를 내뱉고 다시 마시려면 빨대가 2개 필요하다.

 


  • 입력 스트림 비교

- 입력 스트림은 데이터를 먼저 스트림으로 읽어 들인다. 그리고 스트림에 존재하는 데이터를 하나씩 일거 들일 수 있다.

- 음료수를 마실 때 빨대를 이용하여 음료수를 빨대에 모으고 빨대에 들어있는 음료수를 흡입한다. 그러면 음료수가 입안으로 들어 올 것이다.

 

  • 출력 스트림 비교

- 출력 스트림으로 데이터를 보낸다. 출력 스트림에 보낸 데이터를 비워 버린다. 그렇게 되면 출력 스트림에 존재하던 데이터가 모두 목표지점에 저장된다.

- 입 안에 있던 음료수를 빨대로 일단 보낸다. 빨대에 들어있는 음료수를 불어 버린다. 그렇게 되면 음료수는 다시 컵 안으로 들어가게 된다.

 

*출처:http://postitforhooney.tistory.com/entry/Java-Java-Stream%EC%9D%98-%EA%B0%9C%EB%85%90%EA%B3%BC-%EC%A2%85%EB%A5%98-%EA%B7%B8%EB%A6%AC%EA%B3%A0-%EC%98%88%EC%A0%9C%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%B0%A8%EC%9D%B4%EC%A0%90-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0

 

이러한 절차를 스트림으로 표현하면 아래와 같다.

  • 목표로 하는 제이터를 정한다.
  • 데이터에 맞는 스트림을 생성한다.
  • 스트림 클래스의 멤버 메소드를 이용하여 쉽게 데이터를 핸들한다.

   (기록하거나 읽어들이거나, 보내거나 받거나)



스트림의 종류

- 스트림은 크게 문자 단위 처리하느냐, 바이트 단위 처리하느냐에 따라서 나눌 수 있다.

 

- 문자 스트림의 구성도

 입력 문자 스트림은 Reader라는 단어가 붙어있음

△ 출력 문자 스트림은 Writer라는 단어가 붙어있음.

출처: http://elena90.tistory.com/entry/Java-파일-입출력스트림InputStreamOutputStreamReaderWriter [오니님의짱꺤뽀]

바이트 스트림의 구성도

 입력 바이트 스트림은 InputStream라는 단어가 붙어있음.

 

 출력 바이트 스트림은 OutputStream라는 단어가 붙어있습니다. (아닌 것도 있지만 대부분 이 형식에 따름)

출처: http://elena90.tistory.com/entry/Java-파일-입출력스트림InputStreamOutputStreamReaderWriter [오니님의짱꺤뽀]

 

 

입력 스트림 계열의 멤버 메소드

  • 바이트 단위 (InputStream)

- int read()

- int read(byte buf[])

- int read(byte buf[], int offset, int length)

 

 

  • 문자 단위 (Reader)

- int read()

- int read(char buf[])

- int read(char buf[], int offset, int length)

 

출력 스트림 계열의 멤버 메소드

  • 바이트 단위 (OutputStream)

- int write(int c)

- int write(byte buf[])

- int write(byte buf[], int offset, int length)

 

  • 문자 단위 (Writer)

- int write(int c)

- int write(char buf[])

- int write(char buf[], int offset, int length)

문자 스트림

FileInputStreamEXEChar01.java

@FunctionalInterface

interface MyFunction {

    void myMethod();                   

}





class Outer {

    int val = 10;       // Outer.this.val

     

    class Inner {

        int val = 20;   // this.val

         

        void method(int i) {    // void method(final int i) {

            int val = 30;   // final int = 30;

//          i = 10;         // 에러. 상수의 값을 변경할 수 없음.

            MyFunction f = () -> {

                System.out.println("        i : " + i);

                System.out.println("      val : " + val);

                System.out.println(" this.val : " + ++this.val);

                System.out.println("Outer.this.val : " + ++Outer.this.val);

            };



            f.myMethod();

        }

    }// Inner 클래스의 끝

}// Outer클래스의 끝





                 



class RamdaEx08 {



    public static void main(String[] args) {       

        Outer outer = new Outer();

        Outer.Inner inner = outer.new Inner();

        inner.method(100);

    }

}

FileInputStreamEXEChar02.java

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
 
public class FileInputStreamEXEChar02 {
    public static void main(String[] args) throws Exception {
        FileReader fr = new FileReader("C:/Users/DIR-P-0050/Desktop/test.txt");
        BufferedReader br = new BufferedReader(fr);
        String s = null;
        FileWriter fw = new FileWriter("C:/Users/DIR-P-0050/Desktop/testCopys.txt", false);
        BufferedWriter bw = new BufferedWriter(fw);
        while ((s = br.readLine()) != null) {
            bw.write(s);
            bw.newLine();
        }
        fr.close();
        br.close();
        bw.close();
        fw.close();
    }
}

 

InputTest.java

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
 
public class InputTest {
    public static void main(String[] args) throws IOException {
        InputStreamReader isr = new InputStreamReader(System.in);
        BufferedReader br = new BufferedReader(isr);
        String input = null;
        while((input = br.readLine()) != null) {
            if(input.equals("out!")) {
                System.out.println("프로그램을 종료합니다.");
                return;
            }
            System.out.println("입력 : " + input);   
        }
        
         
    }
}

바이트 스트림

FileInputStreamEXEByte01.java

import java.io.FileInputStream;
 
public class FileInputStreanEXEByte01 {
 
    public static void main(String[] args) throws Exception {
        int n = 0;
        FileInputStream fis = new FileInputStream("C:/Users/DIR-P-0050/Desktop/test.txt");
        while ((n = fis.available()) > 0) {
            byte[] b = new byte[n];
            int result = fis.read(b);
            if (result == -1) {
                break;
            }
            String s = new String(b);
            System.out.println("!!");
            System.out.println(s);
        }
        fis.close();
    }
 
}

FileInputStreamEXEByte02.java

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
 
public class FileInputStreanEXEByte02 {
 
    public static void main(String[] args) throws IOException {
        int n = 0;
        FileInputStream fis = new FileInputStream("C:/Users/DIR-P-0050/Desktop/test.txt");
        FileOutputStream fos = new FileOutputStream("C:/Users/DIR-P-0050/Desktop/testCopy.txt", false);
         
        while ((n = fis.available()) > 0) {
            byte[] b = new byte[n];
            int result = fis.read(b);
            if (result == -1) {
                break;
            }
            fos.write(b);
        }
        fis.close();
        fos.close();
         
    }
 
}

 


[추가 내용]

클로저에 대한 간단한 예제 Javascript 

let outerFunction = () => {


    let innerFunction = (a , b ) => {
        return a+ b;
    }

    return innerFunction
}

let closer = outerFunction()

let result = closer(3,5);
console.log(result);

Functional Interface에 여러개의 구현되지 않은 메소드들이 여러개 있을수 있었던 이유

Lamda표현식에서 

@FunctionalInterface
정의시



interfaceFInterface {         //어노테이션 명시 X
publicvoidaaa();
}
 
 
@FunctionalInterface
interfaceFInterface {         //어노테이션 명시 O
publicvoidaaa();
}
 
//-----------------함수형인터페이스 구현의 잘못된 예-----------------
 
@FunctionalInterface
interfaceFInterface {         //두 개 이상의 추상 매서드를 가질경우 에러
publicvoidaaa();
publicvoidbbb();
}
위와 같이 설명하였지만
Compare인터페이스를 보면 추상메서드가 2개이상 정의 되어있는것을 볼수있는데 
Lamda표현식에서 Object의 기본 메소드를 제외하고 규칙이 성립된다.
참고 예제 : RamdaEx05_1.java