상세 컨텐츠

본문 제목

[Java] 7. 메소드 참조 (::)

프로그래밍 언어/Java(자바)

by Guroo 2023. 8. 31. 14:37

본문

6. Map에 정의된 추상 메소드와 default 메소드

 

6. Map에 정의된 추상 메소드와 default 메소드

6-1. Map에 정의된 추상 메소드와 default 메소드 이번에 살펴볼 인터페이스는 Map이다. Map은 아래의 그림에서 보는 것과 같이 데이터 저장을 위한 Key와 Value의 쌍으로 구성되어 있는 형태의 데이터

sesoc.tistory.com

 

이 번에 살펴볼 내용은 자바 8버전에서 새롭게 지원되는 ::(이중 콜론) 연산자이다.

더블 콜론연산자는 말 그대로 콜론 문자를 두 개를 연속으로 붙여 사용하는 연산자로 Lambda식을 축약하여 사용할 수 있다.

메소드 참조는 람다식에서만 사용 가능하며 아래와 같은 형태로 사용한다.

클래스명::메소드명
인스턴스::메소드명

 

1) 객체의 메소드를 호출하는 방법

 

:: 연산자를 사용하면 객체의 메소드를 호출하는 람다식을 다음과 같이 변경하여 사용할 수 있다.

 

// Lambda식을 이용한 방법
(x) -> x.toUpperCase();

// :: 연산자를 이용한 방법
String::toUpperCase;

 

이 연산자를 사용할 때는

 

:: 연산자는 객체의 메소드를 호출하거나 인터페이스의 추상 메소드를 구현하는 경우에 사용한다.

:: 연산자를 사용하면 람다식의 실행문은 메소드나 인터페이스의 추상 메소드의 구현과 같아야 한다.

 

 

:: 연산자 즉, 메소드 참조는 람다식에서 파라미터와 해당 파라미터를 사용하는 메소드의 전달인자가 같을 때 사용하며 이 연산자를 사용하면 코드가 짧아지고 간결해진다.

 

예를 들어 아래와 같이 문자열 리스트가 있을 때 문자열 배열을 순회하며 모든 문자열을 출력하려면 일반적인 for문을 사용하거나 아래와 같이 람다식을 사용할 수 있다.

List<String> list = List.of("태조", "정종", "태종", "세종", "문종", "단종", "세조");
list.forEach(x -> System.out.println(x));
        
list.forEach(System.out::println); // 위와 동일

 

위 코드에서 forEach() 메소드의 전달인자로 람다식을 사용하는데, 이 람다식을 살펴보면 전달인자 x, 전달된 x를 출력하는 System.out.println()에서의 출력값도 x이다.

 

이렇게 전달인자와 파라미터가 동일한 경우 메소드 참조의 형태로 변경할 수 있는 변경하는 순서(?) 는

 

1. 전달인자 x 삭제,

(-> System.out.println(x));

 

2. 화살표 삭제,

(System.out.println(x));

 

3. 실행문의 괄호와 매개변수 삭제,

(System.out.println);

 

4. 클래스변수와 메소드 사이 :: 삽입

(System.out::println);

 

 

다음의 예를 통해 다양한 메소드 참조를 사용방법을 확인해보자.

 

MethodReference1.java

import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;

class Change {
    // 전달받은 문자열의 첫 글자는 대문자, 나머지는 소문자로 변경한 후
    // 반환하는 메소드.
    static String toChage(String temp) {
        return temp.substring(0, 1).toUpperCase() 
                + temp.substring(1).toLowerCase();
    }
}

public class MethodReference1 {
    public static void main(String[] args) {
        // 예1) Math 클래스에 선언된 static 메소드인 floor()
        Function<Double, Double> d1 = x -> Math.floor(x);
        
        // 메소드 참조형으로 변경
        Function<Double, Double> d2 = Math::floor;
        System.out.println(d2.apply(42.195));
        
        // 예2) String 클래스의 인스턴스 메소드인 toUpperCase()  
        //    문자열을 전달하면 대문자로 변환하는 Function을 정의
        //    f3와 동일하게 동작하는 f4를 메소드 참조형태로 변경 
        Function<String, String> f3 = a -> a.toUpperCase();
        Function<String, String> f4 = String::toUpperCase;
        
        String theRose = """
                and the soul
                afraid of dying
                that never learns to live""";

        System.out.println(f4.apply(theRose));

        // 예3) 사용자가 생성한 Change클래스의 정적메소드를 이용한 메소드 참조
        String theRose2 = """
                Just remember in the winter 
                Far beneath the bitter snows 
                Lies the seed that with the sun's love 
                In the spring becomes the rose""";
        
        // 문자열을 전달하면 다시 문자열로 반환받는 Function 클래스를 정의한다.
        Function<String, String> c = a -> Change.toChage(a);
        // 메소드 참조형으로 변경된 형태
        Function<String, String> c1 = Change::toChage;

        System.out.println(c1.apply("KOREA")); // 동작이 되는 것을 확인

        // 위에 정의된 문자열의 마지막에 포함된 엔터를 빈 문자열로 변경하여 한 줄로 만든다. 
        String s = theRose2.replace("\n" , " ");
        
        // 문자열을 분리하여 List로
        List<String> l1 = List.of(theRose2.split(" "));
        
        // 분리된 모든 List 내 단어를 첫 글자를 대문자로 바꾸어 출력  
        l1.forEach(x -> System.out.print(c1.apply(x) +" "));
        System.out.println("\n");
        
        // 예4) 문자열 2개를 전달받아 정수로 변환하는 BiFunction을 정의
        BiFunction<String, String, Integer> b1 = (a, b) -> a.compareTo(b);
        BiFunction<String, String, Integer> b2 = String::compareTo;
        System.out.println(b2.apply("A", "a")); 

        // 예5) 사용자가 정의한 sum() 메소드 호출
        List<String> iList = List.of("23", "45", "1", "56", "22");
        System.out.println("수치 데이터총합: " + sum(Integer::parseInt, iList));
    }

    public static int sum(Function<String, Integer> f, List<String> iList) {
        int total = 0;
        
        for(String s : iList) {
            total += f.apply(s);
        }
        
        return total;
    }
}

 

이번에는 날짜를 이용한 메소드 참조의 예를 보려고 한다.

 

1.7버전 이전에는 날짜 데이터를 사용하기 위해 Calendar 라는 클래스를 사용하였다.

추후 다시 살펴보겠지만 자바 1.8버전에 추가된 기능 중에 날짜나 시간을 다루는 클래스로 java.time 패키지의 LocalDate, LocalTime 등이 있다.

 

이전 버전에서 사용했던 Calendar 클래스의 각종 메소드들보다 더 정교하게 날짜를 다룰 수 있도록 추가된 기능인데 이 외에도 java.time 패키지에는 날짜를 다룰 수 있는 다양한 클래스들이 존재한다.

 

아래의 예는 이 중 날짜 관련된 몇 개의 클래스와 메소드를 사용한 것이다.

 

MethodReference2.java

import java.time.LocalDate;
import java.time.Period;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;

public class MethodReference2 {
    public static void main(String[] args) {
        // 예6) LocalDate 클래스와 두 날짜 사이의 기간을 구하는 클래스를 이용한 메소드 참조
        LocalDate war = LocalDate.of(1948, 8, 15);  // 광복일
        LocalDate today = LocalDate.now();          // 오늘
        
        Period period = Period.between(war, today); // 두 날짜 사이 기간을 구하는 클래스
        
        BiFunction<LocalDate, LocalDate, Period> t = (x, y) ->  Period.between(x, y);
        BiFunction<LocalDate, LocalDate, Period> t1 = Period::between;
        
        System.out.println("광복 된지 " + t1.apply(war, today).getYears() +"년이 지남");
        
    }
}

  

이번에는 사용자가 정의한 클래스 내의 메소드를 :: 로 사용하는 예를 짧은 코드로 확인해 보자

 

아래의 코드를 살펴보면 Score 클래스에는 생성자와 점수에 따른 합, 불 결과를 반환하는 정적 메소드가 선언되어 있다.

Predicate 인터페이스를 람다식과 메소드 참조형으로 변환한 예가 있고, 29번 줄에는 이를 이용하여 학생들의 결과를 출력하는 forEach() 문이 있다.

 

그리고 passCount() 라는 메소드는 메소드 참조형으로 선언된 람다식을 파라미터로 받아 합격자 인원수를 세서 반환해주는 코드이다.

 

MethodReference3.java

import java.util.List;
import java.util.function.Predicate;

class Score {
    String name;
    double total;
    public Score(String name, double total) {
        this.name = name;
        this.total = total;
    }
    static boolean isPass(Score s) {
        return s.total > 70;
    }
}
public class MethodReference3 {
    static List<Score> list = List.of(
        new Score("홍길동", 89),
        new Score("전우치", 69),
        new Score("사오정", 84),
        new Score("이몽룡", 50),
        new Score("손오공", 100)
    ); 
    
    public static void main(String[] args) {
        // 합격 여부를 true/false로 반환하는 isPass() 메소드를 참조형태로 변경
        Predicate<Score> p1 = (s) -> (Score.isPass(s));
        Predicate<Score> p2 = Score::isPass;
        
        list.forEach(x->System.out.println(x.name +":" + (p2.test(x)?"합격":"불합격")));
        
        int count = 0; 
        count = passCount(Score :: isPass);
        System.out.println("총 합격자 수: " + count);
    }
    
    // 합격자 인원수를 구하는 메소드
    public static int passCount(Predicate<Score> p2) {
        int count = 0;
        for(Score x : list)
            count += p2.test(x) ? 1 :  0;
        
        return count;
    }
}

 

여기까지 Lamda에서 사용하는 메소드 참조에 대해 간단히 살펴보았다.

다음에는 1.8에 추가된 주요 기능 중 하나인 Stream에 대해 공부해 보자


이전 글: [Java] 6. Map에 정의된 추상 메소드와 default 메소드

다음 글: [Java] 8. Stream의 개념과 사용 방법-1 

 

관련글 더보기

댓글 영역