출처 : 윤성우의 열혈 Java 프로그래밍
public class Main {
class Inner{
}
static class Nested{
}
public static void main(String[] args) {
System.out.println("Hello world!");
}
}
클래스 내의 클래스를 nested class라 한다.
nested 클래스 중 static 선언이 되지 않은 클래스를 inner 클래스라 한다.
이너 클래스는 세가지로 나뉜다
static 선언이 갖는 특성이 반영된 클래스이다
class Outer{
private static int num = 0;
static class Nested{
public void add(int n){
num+=n;
}
}
}
public class Main {
public static void main(String[] args) {
Outer.Nested n = new Outer.Nested();
n.add(1);
}
}
위와 같이 외부 클래스 이름을 포함한 형태로 객체를 생성한다.
그리고 static nested class의 인스턴스 생성은 외부 클래스 인스턴스를 생성하지 않고도 생성할 수 있다.
때문에 static nested class에서는 외부 클래스의 static으로 선언된 변수와 메소드에만 접근이 가능하다.
네스티드 클래스 중 static 선언이 붙지 않은 것이 inner class이다.
이너 클래스는 멤버 클래스, 로컬 클래스, 익명 클래스로 나뉜다.
멤버 클래스는 인스턴스 변수, 인스턴스 메소드와 동일한 위치에 정의된 클래스이다.
로컬 클래스는 중괄호 내(특히 메소드 내)에 정의된 클래스이다.
class Outer{
class MemberClass{
}
void method(){
class LocalClass{
}
}
}
예시는 위와 같다.
interface Printable{
void print();
}
class Papers{
private String con;
public Papers(String s) {con=s;}
public Printable getPrinter(){
return new Printable(){
public void print(){
System.out.println(con);
}
};
}
}
public class Main {
public static void main(String[] args) {
Papers p = new Papers("서류");
Printable prn = p.getPrinter();
prn.print();
}
}
익명 클래스는 위 예시를 통해 이해할 수 있다.
getPrinter()
메소드 내부를 보면 Printable
은 인터페이스인데, 인터페이스를 생성하면서 구현하는 클래스의 정의를 덧붙여서 인스턴스 생성이 가능하게 했다.
람다를 사용하면 코드를 줄일 수 있고 가독성도 뛰어나다
익명 클래스 예시에서 사용한 코드를 람다로 줄일 수 있다.
interface Printable{ //추상 메소드가 하나인 인터페이스
void print(String s);
}
public class Main {
public static void main(String[] args) {
Printable prn = (s)->System.out.println(s); //람다식
prn.print("서류");
}
}
(s)->System.out.println(s)
부분이 람다식이다.
람다와 익명 클래스는 다르다. 하지만 둘 다 인스턴스의 생성으로 이어진다.
핵심은 Printable이 추상 메소드가 하나뿐인 인터페이스라는 것이다.
람다식을 메소드의 인자로 전달할 수 있다.
method((s)->System.out.println(s));
예시는 아래와 같다
interface Printable{ //추상 메소드가 하나인 인터페이스
void print(String s);
}
public class Main {
public static void ShowString(Printable p, String s){
p.print(s);
}
public static void main(String[] args) {
ShowString((s)->{System.out.println(s);},"서류");
}
}
예시에서 ShowString(Printable p, String s)
의 첫번째 인자로 람다식을 전달해서 첫번째 매개변수인 Printable p
를 초기화한다
즉 Printable p = (s)->{System.out.println(s);}
로 초기화 한 것과 같다.
람다는 인스턴스보다 기능 하나가 필요한 상황을 위해 사용한다.
프로그램을 작성하다 보면 기능 하나를 정의해서 전달해야 하는 상황이 있다.
예를 들어 Comparator<T>
인터페이스의 구현이 필요한 상황이다.
import java.util.*;
class SLencomp implements Comparator<String>{
@Override
public int compare(String s1, String s2) {
return s1.length()-s2.length(); //짧은게 앞에 오도록
}
}
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Robot");
list.add("Lambda");
list.add("Box");
Collections.sort(list, new SLencomp()); // 정렬
for (String s: list){
System.out.println(s);
}
}
}
위 코드를 보면 Collections.sort()
를 호출하면서 두번째 인자로 정렬의 기준을 갖는 SLenComp
객체를 생성해서 전달한다.
객체를 전달하지만, 필요한 것은 객체 내에 있는 메소드 기능을 전달하는 것이다.
import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Robot");
list.add("Lambda");
list.add("Box");
Collections.sort(list, (s1,s2)->s1.length()-s2.length()); // 정렬
for (String s: list){
System.out.println(s);
}
}
}
이런 식으로 람다식을 사용해서 기능을 전달하도록 변경할 수 있다.
interface Printable{
void print(String s); //매개변수 하나, 반환형 void인 메소드
}
public class Main {
public static void main(String[] args) {
Printable p;
//줄임 없는 표현
p = (String s)->{System.out.println(s);};
//중괄호 생략
p = (String s)-> System.out.println(s);
//매개변수 형 생략
p = (s)->System.out.println(s);
//매개변수 소괄호 생략
p = s->System.out.println(s);
}
}
;
도 같이 지운다.String
형인 것은 컴파일러 입장에서 유추가 가능하므로 생략이 가능하다interface Calculate{
void cal(int a, int b);
}
public class Main {
public static void main(String[] args) {
Calculate c;
c = (a,b)->System.out.println(a+b);
c = (a,b)->System.out.println(a-b);
}
}
형태는 다음과 같다.
마찬가지로 몸체 문장이 1개여서 중괄호를 생략했다
interface Calculate{
int cal(int a, int b);
}
public class Main {
public static void main(String[] args) {
Calculate c;
c = (a,b)-> {return a+b;};
c = (a,b) -> a+b;
}
}
첫번째 람다식의 경우 메소드 몸체에 해당하는 내용이 return문이면 그 문장이 하나더라도 중괄호의 생략이 불가능하다.
두번째 람다식은 메소드 몸체에 연산이 등장하는데 이 연산의 결과로 값이 남게 되고 그럼 별도로 명시하지 않아도 반환의 대상이 된다.
따라서 return문이 메소드 몸체를 이루는 유일한 문장이면 이렇게 작성할 수 있고, 이게 보편적인 방식이다.
import java.util.Random;
interface Generator{
int rand();
}
public class Main {
public static void main(String[] args) {
Generator gen = ()->{
Random rand = new Random();
return rand.nextInt();
};
}
}
매개변수가 없는 람다식은 매개변수를 표현하는 소괄호 안을 비우면 된다.
그리고 참고할 점은 메소드 몸체가 둘 이상의 문장이므로 반드시 중괄호로 감싸주어야 하고 return
으로 반환해야 한다.
지금까지 확인한 예제들을 보면 인터페이스에 추상 메소드가 딱 하나만 선언되어 있다. 이러한 인터페이스들을 함수형 인터페이스라 하고 람다식은 함수형 인터페이스를 기반으로만 작성될 수 있다.
@FunctionalInterface
애노테이션으로 함수형 인터페이스가 맞는지 확인할 수 있다.@FunctionalInterface
interface Calculate<T>{ //제네릭 기반의 함수형 인터페이스
T cal(T a, T b);
}
public class Main {
public static void main(String[] args) {
Calculate<Integer> ci = (a,b)->a+b;
Calculate<Double> cd = (a,b)->a+b;
}
}
다음과 같이 제네릭 기반으로 정의된 함수형 인터페이스에도 람다식을 사용할 수 있다.
@FunctionalInterface
interface Calculate<T>{ //제네릭 기반의 함수형 인터페이스
T cal(T a, T b);
}
public class Main {
public static <T> void calAndShow(Calculate<T> op, T n1, T n2){
T r = op.cal(n1,n2);
System.out.println(r);
}
public static void main(String[] args) {
calAndShow((a, b)->a+b,3,4);
calAndShow((a,b)->a+b, 2.5,7.1);
calAndShow((a,b)->a-b,4,2);
calAndShow((a,b)->a-b,4.9,3.2);
}
}
이런 식으로 제네릭 타입이 유추가 가능한 상황에서는 위와 같이 람다식을 작성할 수 있다.
정말 깊이 있는 글이었습니다.