본문 바로가기

programming/Java [notion 정리본 업로드]

중첩 클래스와 람다

A. 중첩 클래스/인터페이스 용어정리

A-1. 용어 정리

  • 🍯Tip : 들어가기 전에.. (변수 선언)
    1. 필드 : 클래스의 멤버로 선언하는 변수. 접근 수식어(public, private, …)를 사용할 수 있음
      1. 인스턴스 필드 : static 이 아닌 필드
        • 객체를 생성한 후에, 그 참조 변수로 접근해서 사용하는 필드
        • 객체 생성자 생성 시, 메모리에 생성됨
      2. 클래스 필드 : static으로 선언된 필드
        • 프로그램이 시작될 때, 클래스 로더에 의해서 메모리에 미리 로딩되는 변수
        • 객체를 생성하지 않고, 클래스 이름을 접두사로 사용함.
    2. 지역 변수 : 메서드 안에서 선언하는 변수. 접근 수식어를 사용할 수 없음.
      1. 선언된 위치에서부터 변수가 포함된 블록이 끝날 때까지 사용 가능
      2. 파라미터도 지역 변수의 한 종류
      3. 지역 변수는 static을 사용할 수 없음 </aside>
  • <aside> 💡 🚨🚨🚨변수 선언🚨🚨🚨 (클래스와 비슷)

외부 클래스/인터페이스

outer, enclosing class/interface

  • 내부 클래스/인터페이스를 감싸고 있는 클래스/인터페이스

내부 클래스/인터페이스

inner class/interface

  • 다른 클래스/인터페이스 안에서 선언된 클래스/인터페이스
  1. 멤버 내부 클래스
    1. 인스턴스 내부 클래스 : static이 아닌 내부 클래스
      • 외부 클래스의 객체를 우선 생성한 후에, 그 참조 변수를 사용해서 객체를 생성할 수 있는 내부 클래스.
      • 외부인스턴스.생성자 호출()
    2. static 내부 클래스 : static으로 선언된 내부 클래스
      • 외부 클래스의 객체 생성 여부와 상관 없이 사용할 수 있는 클래스
      • 중첩 클래스(nested class) 교육자에 따라 중첩 클래스가 전체를 포괄하는 경우가 있고, static 내부 클래스만 중첩 클래스로 분류하는 경우가 있다.
  2. 지역 (내부) 클래스 (local class)
    • 메서드 안에서 선언된 내부 클래스
    • 선언된 블록 안에서만 객체를 생성하고 사용할 수 있는 클래스
  3. 익명 (내부) 클래스(anonymous class)
    • 보통 지역 클래스인 경우가 많음.
    • 클래스 선언과 동시에 객체 생성까지 해야만 하는 클래스
    • → 람다 표현식.
    • 자바에서 가장 많이 사용하고 있는 내부 클래스로 잘 알아놓는 것이 좋음.

A-2. 인스턴스 내부 클래스

내부 클래스 내에서 외부 클래스 멤버에 접근하기

package com.itwill.inner01;

public class Outer {
//	field
	private int x;
	private int y;
	private String s;

//	constructor
	public Outer(int x, int y, String s) {
		this.x = x;
		this.y = y;
		this.s = s;
	}

	public int getX() {
		return x;
	}

//	method
	@Override
	public String toString() {
		return String.format("Outer(x=%d, y=%d, s=%s)", this.x, this.y, this.s);
	}

//	static이 아닌 내부 클래스 선언
	public class Inner {
//		field
		private int y;

//		constructor
		public Inner(int y) {
			this.y = y;
		}

//		method
		public void info() {
			System.out.println("--- Inner Class ---");
			System.out.println("y = " + y);
			System.out.println("Outer x = " + x); // (1)
			System.out.println("Outer y = " + Outer.this.y); // (2)
			System.out.println("Outer s = " + s);
			System.out.println(Outer.this.toString()); // 외부 클래스의 메서드를 사용할 수 있다.
			
		}
	}

}
  • 외부 클래스와 내부 클래스의 필드명(y)이 겹친다.
  1. 내부 클래스는 외부 클래스의 멤버를 접근할 수 있음
  2. 멤버의 이름이 겹치는 경우, 외부 클래스의 필드에 접근하기 위해 Outer.this. 접두사를 붙인다.
  3. 2와 동일하게 이름이 겹치는 메서드를 호출할 시에도, Outer.this. 접두사를 이용하여 외부 메서드를 접근할 수 있다.

메인에서 객체 생성

public class InnerMain01 {
	public static void main(String[] args) {
//		Outer 클래스 타입 객체 생성
		Outer outer1 = new Outer(1, 2, "java");

		System.out.println(outer1);

//		Outer.Inner 클래스 타입 객체 생성
		Outer.Inner inner1 = outer1.new Inner(100); // (1)
		inner1.info();

		Outer.Inner inner2 = outer1.new Inner(123);
		inner2.info();

		System.out.println(inner1.getX()); // (2)
	}
}
  • 내부 클래스의 인스턴스와 그를 생성하기 위해 사용된 outer1 의 변수의 객체는 어떠한 관계도 없다. 그렇기 때문에 클래스 밖에서 내부 클래스의 인스턴스로 outer1의 변수나 메서드에 접근할 수 없다.
    • 다만 클래스 멤버 내에서는 외부 클래스의 변수나 메서드에 접근할 수 있으므로 해당 기능이 필요한 경우에는 내부 클래스의 메서드 등으로 이를 활용하면 된다.
  1. 내부 클래스 생성자 호출 문법
  2. 상속과 내부 클래스의 차이
    • 클래스 외부에서 외부 클래스의 메서드나 필드에 접근할 수 없다.
  • inner 인스턴스의 변수 Type을 Outer.Inner 처럼 길게 사용하지 않고, import를 사용하여 더 짧게 사용할 수 있다.
package com.itwill.inner01;

import com.itwill.inner01.Outer.Inner;

public class InnerMain01 {
		Inner inner4 = outer1.new Inner(3);
	}
}

A-3. static 내부 클래스 (a.k.a 중첩 클래스)

package com.itwill.inner01;

public class Enclosing {
	public static int var = 1; // static field
	private int x; // instance field

//	constructor
	public Enclosing(int x) {
		this.x = x;
	}

//	static method
	public static void test() {
		System.out.println("var = " + var);
//		System.out.println("x = " + x); // -> (1)
	}

//	instance method

	@Override
	public String toString() {
		return String.format("Enclosing(var = %d, x = %d)", var, x); // (2)
	}

	public static class Nested {
		private int x;

		public Nested(int x) {
			this.x = x;
		}

		public void info() {
			System.out.println("---- Nested Class ----");
			System.out.println("x = " + x);
//			System.out.println(Enclosing.this.x; // (3)
		}
	}

} // class Enclosing
  1. 컴파일 에러
    • static 멤버는 다른 static 멤버들만 사용가능, non-static 멤버는 사용 불가
  2. static이 아닌 인스턴스 메서드는 static과 non-static 멤버들을 모두 사용할 수 있음.
  3. 컴파일 에서
    • static 내부 클래스에서는 외부 클래스의 non-static 멤버를 사용할 수 없음!

중첩 클래스 생성하기

Enclosing.Nested nested = new Enclosing.Nested(1);
  • Enclosing Class와 Nested Class는 문법 상으로 관련이 없는 두 개의 클래스를 겹쳐놓은 것이라고 생각하면 된다.
  • import하여 타입 더 짧게 작성하여 중첩 클래스 인스턴스 사용하기
import com.itwill.inner01.Enclosing.Nested;
------------------------------------------------------------------------
public static void main (String[] args) {
	Enclosing.Nested nested = new Enclosing.Nested(1);
	Nested nested2 = new Nested(0);
}

빌더(Builder, Factory) Design 패턴

**package com.itwill.inner02;

public class Book {
//	field 
	private String title; // 책 제목
	private String author; // 책 저자
	private String publisher; // 출판사

	public Book() {
	}

	public Book(String title) {
//		this.title = title;
		this(title, null, null);
	}

	public Book(String title, String author) {
		this(title, author, null);
	}

	public Book(String title, String author, String publisher) {
		this.title = title;
		this.author = author;
		this.publisher = publisher;
	}
}**
  • 생성자 오버로딩
    • 파라미터 (개수 타입)이 다른 생성자들을 여러 개 작성
    • Book(String title, String author) 생성자가 있는 경우. Book(String titls, String publisher) 생성자는 오버로딩할 수 없음.
    • 동일 타입 필드가 여러 개 있을 경우, 아규먼트 수가 똑같다면 만들 수 있는 생성자의 종류는 제한 적임
    • 또한 개발자가 개발 시 타입으로만 구분하기에 혼동할 수 있는 여지가 충분
    ⇒Builder 클래스를 만들어 여러 조합으로 외부 클래스를 생성할 수 있도록 도와줌
public class Book {
//	field 
	private String title; // 책 제목
	private String author; // 책 저자
	private String publisher; // 출판사

//	------------ Builder(Factory) 디자인 패턴 ---------------------- //
	public static BookBuilder builder() {
		return new BookBuilder();
	}  

	public static class BookBuilder {    // (1)
		private String title;
		private String author;
		private String publisher;

		private BookBuilder() {
		}

		public BookBuilder title(String title) {
			this.title = title;
			return this;
		}

		public BookBuilder author(String author) {
			this.author = author;
			return this;
		}

		public BookBuilder publisher(String publisher) {
			this.publisher = publisher;
			return this;
		}   // (2)

		public Book build() {
			return new Book(title, author, publisher);
		}

	} // class BookBuilder
  1. Book을 만들기 위해 BookBuilder를 사용할 수 있어야 한다.
    • BookBuilder는 Book을 생성하기 전에 접근할 수 있어야 한다.
    • 그렇기 때문에 BookBuilder 클래스와 Book 내부에 선언된 BookBuiler의 인스턴스를 반환하는 메서드는 static 메서드여야 한다.
  2. 연쇄적으로 메서드를 사용할 수 있게 하기 위해서 return this; 를 하여 실제 BookBuilder를 사용하여 Book의 인스턴스를 만들기 위해서든 다음과 같이 사용된다.
package com.itwill.inner02;

public class InnerMain02 {
	public static void main(String[] args) {
		Book book1 = new Book("일론 머스크", "월터 아이작슨", "20세기북스");
		System.out.println(book1);

		Book book2 = new Book("일론 머스크", "월터 아이작슨");
		System.out.println(book2);

		Book book3 = Book.builder()
				
				.author("윌터아이작슨").title("일론머스크").publisher("20세기북스")
				
				.build();
		
		System.out.println(book3);

		Book book4 = Book.builder().author("윌터아이작슨").build();
		System.out.println(book4);
	}

}
  • 생성자 오버로딩이 불가능하여 사용하지 못했던 author 정보만 가지고 있는 Book의 인스턴스를 Builder Class를 사용하여 만들 수 있다.
  • 또한 순서에 상관없이 메서드를 이용하여 생성 필드들을 필요한 순서대로 입력할 수 있다.

A-4. 지역 클래스

  • 내부 인터페이스는 항상 public static 임
package com.itwill.inner03;

public class Button {
//	public static 내부 인터페이스 - static은 생략 가능
	interface OnClickListener {
		void onClick(); // public abstract 메서드

	}

//	field
	private String btnName;
	private OnClickListener listener;

//	constructor
	public Button(String btnName) {
		this.btnName = btnName;
	}

//	setter
	public void setOnClickListener(OnClickListener listener) {
		this.listener = listener;
	}

//	method (기능)
	public void click() {
		System.out.println(btnName + "버튼 : ");
		listener.onClick(); // 버튼을 클릭
	}
}
package com.itwill.inner03;

import com.itwill.inner03.Button.OnClickListener;

public class OpenButtonListener implements OnClickListener {

	@Override
	public void onClick() {
		System.out.println("파일 열기 실행");
	}

}
package com.itwill.inner03;

public class InnerMain03 {
	public static void main(String[] args) {
//		field 

		Button btnOpen = new Button("열기");
//		버튼이 할일을 가지고 있는 객체를 생성

//		리스너 객체를 생성
		OpenButtonListener listener = new OpenButtonListener();

//		리스너 객체를 버튼에 등록
		btnOpen.setOnClickListener(listener);

		btnOpen.click(); // 버튼을 클릭
	}
}
  • 위 코드는 중복해서 사용하지 않고, 단 세 줄만 추가하기 위해 별도의 클래스를 구성하여 효율적이지 않음
  • 한 번만 사용할 이 OpenButtonListener 타입의 클래스를 따로 구성하지 않고, 다음과 같이 Main 메서드 내에서 지역 클래스를 구현하여 해당 지역 내에서만 사용할 클래스를 생성할 수 있음.
    • 메서드 내에 선언된 클래스의 경우, 지역 클래스가 생성된 코드 위에 생성자를 부를 수 없다.
      • 메서드는 위에서 아래로 시작된다..
  • package com.itwill.inner03; import com.itwill.inner03.Button.OnClickListener; public class InnerMain03 { // 1. Button 타입 객체 생성 Button btnClose = new Button("닫기"); // 2. OnClickListener를 구현하는 클래스 선언 -> 지역 클래스 class CloseButtonListener implements OnClickListener { @Override public void onClick() { System.out.println("파일 닫기를 실행합니다.."); } } // 3. 리스너 객체 생성 OnClickListener closeListener = new CloseButtonListener(); // 4. 버튼에 리스너를 설정 btnClose.setOnClickListener(closeListener); btnClose.click(); }
  • 클래스에 이름을 붙이는 이유는 생성자 호출이나 타입 지정을 위한 것인데, 하나의 객체를 생성할 목적으로 생성하는 클래스는 따로 이름을 붙일 필요가 없지 않을까라는 고민에서 익명 클래스가 나왔다.

A-5. 익명 클래스

package com.itwill.inner03;

import com.itwill.inner03.Button.OnClickListener;

public class InnerMain03 {
	public static void main(String[] args) {
		System.out.println("--- 익명 클래스 ---");

//		1. 버튼 타입 객체 생성
		Button saveButton = new Button("저장");

//		2. 리스너 객체 생성
		**OnClickListener saveListener = new OnClickListener() {
			@Override
			public void onClick() {
				System.out.println("파일을 저장합니다..");
			}
		};**
		
		saveButton.setOnClickListener(saveListener);
		saveButton.click();
	}
}
  • 클래스 선언과 동시에 클래스 생성을 할 수 있는 익명 클래스

A-6. Lambda와 익명 클래스 구현

중첩 인터페이스는 static이 없어도 항상 static

함수형 인터페이스

  • 추상 메서드를 항상 1개만 갖는 인터페이스
  • @FunctionalInterface라는 annotation을 붙일 수 있음 :
    • 선택사항이지만, 컴파일러가 확인해줄 수 있으므로 쓰는게 좋지 않을까?!

<aside> 💡 🚨🚨함수형 인터페이스 구현 방법 4가지🚨🚨

package com.itwill.lambda01;

public class Calculator {

	@FunctionalInterface
	public interface Calculable {
		double calculate(double x, double y);
	}

	private double x;
	private double y;

	public Calculator(double x, double y) {
		this.x = x;
		this.y = y;
	}
	
	public double calculate(Calculable calc) {
		return calc.calculate(x, y);
	}
}
  • Calculator 클래스 내의 calculate 메서드를 사용하기 위해서는 함수형 인터페이스 Calculable을 구현한 클래스의 객체를 인자로 넘겨야 한다.
  • 함수형 인터페이스를 구현하여 객체를 넘기는 4가지 방법에 대해서 알아보자
    • 람다 표현식 예시는 아래에서 확인하세요~
Calculator calc = new Calculator(2, 1);

Calculable adder = new Adder();
double result = calc.calculate(adder);
System.out.println("result = " + result);
Calculator calc = new Calculator(2, 1);

class Subtractor implements Calculable {
	@Override
	public double calculate(double x, double y) {
		return x - y;
	}
}
//		Subtractor 타입 객체를 calculate 메서드 아규먼트로 전달
result = calc.calculate(new Subtractor());
Calculator calc = new Calculator(2, 1);

result = calc.calculate(new Calculable() {
	@Override
	public double calculate(double x, double y) {
		return x * y;
	}
});

</aside>

람다 표현식(Lambda expression) - 함수형 표기법

  • 클래스 선언과 객체 생성을 동시에 하는 익명 클래스를 간단히 작성하기 위한 문법
  • 함수형 인터페이스 타입 객체만 람다 표현식으로 작성할 수 있음.

문법

(파라미터 선언) -> { 메서드 코드; }  // (1)
(double x, double y) -> { return x / y; }

(파라미터 선언) -> 메서드 코드
  1. 기본 문법
  2. 생략 표현 (조건 : 실행 문장이 하나만 있는 경우)
  • 람다 표현식의 파라미터 선언에서는 변수 타입을 생략할 수 있음 (명시해줘도 됨)
  • 람다 표현식에서 메서드 본체의 문장이 하나만 있는 경우에는 {}와 ;를 생략할 수 있음.
  • 람다표현식이 return 문장만 있는 경우에는 {}, return, ; 을 모두 생략하고 리턴 값만 표기
    • EX) (x, y) → x + y
  • 람다 표현식에서 파라미터가 1개만 있는 경우에는 ()도 생략 가능
    • EX) x → 2 * x + 3
  • FunctionalInterface를 구현할 때만 사용할 수 있음
    • 구현해야 할 메서드가 1개만 있는 인터페이스
  • 호출할 수 있는 메서드가 1개밖에 없기 때문에 메서드 이름은 중요하지 않음
Calculator calc = new Calculator(2, 1);

result = calc.calculate((x, y) -> x / y);
System.out.println("result = " + result);

<aside> 💡 람다에

</aside>

Filter 함수 구현

package com.itwill.lambda02;

@FunctionalInterface
public interface MyFilter {
	boolean test(Object x);
}
package com.itwill.lambda02;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class LambdaMain02 {

	public List**<? extends Object>** filter(List<Object> list, MyFilter filter) { // (1)
		List<Object> result = new ArrayList<Object>();

		for (Object x : list) {
			if (filter.test(x))
				result.add(x); // ㅋㅋ result를 넣고 있었음;;;
		}
		return result;
	}

	public static void main(String[] args) {
//		LambdaMain02 타입 객체 생성
		LambdaMain02 app = new LambdaMain02();

		List<Object> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7);
		System.out.println(numbers);

		List<? extends Object> evens = app.filter(numbers, new MyFilter() {
			@Override
			public boolean test(Object x) {
				return ((Integer) x) % 2 == 0;
			}
		});

		System.out.println(evens);
		System.out.println(evens.get(0) instanceof Integer);

// (2)
		List<? extends Object> odds = app.filter(numbers, x -> (Integer) x % 2 != 0);
		System.out.println(odds);

		List<Object> languages = Arrays.asList("Java", "SQL", "JavaScript", "HTML", "Servlet");
		System.out.println(languages);

//		languages의 원소들 중 5글자 이상인 문자열들로 이루어진 리스트를 만들고 출력
		List<Object> result = app.filter(languages, (x) -> x instanceof String ? ((String) x).length() >= 5 : false);

		System.out.println(result);
	}

}
  • 메인 함수 내에 구현된 filter 함수는 Filter 기준을 반환해주는 MyFilter 타입의 아규먼트가 필요함.
  1. Bold로 작성된 <? extends Object> 를 사용하면 Object를 상속받은 하위 타입 인스턴스면 어느 것이든지 반환값으로 올 수 있음.
    • <?> 는 어떤 타입의 객체든 반환값으로 올 수 있음
    • 하지만 List과 같은 Collection Framework 내의 원소타입으로만 해당 문법을 사용할 수 있고(Maybe Generic), int main () {} 과 같이 ? main () {} 으로는 사용할 수 없다.
  2. numbers의 원소들 중에서 홀수들로만 이루어진 리스트를 만들고 출력

B. Stream

  • 통로의 역할

메서드 참조

List<String> languages = Arrays.asList("Java", "JavaScript", "Python", "Kotlin", "C");
System.out.println(languages);
List<String> results 
	= languages.stream().filter(x -> x.length() >= 5)**.map(x -> x.toLowerCase())**.toList();
	= languages.stream().filter(x -> x.length() >= 5)**.map(String::toLowerCase)**.toList();
System.out.println(results);
  • 볼드의 내용은 String::toLowerCase 라고도 사용하고, 메서드 참조라고 한다.
  • 람다 표현식이 아규먼트를 1개만 갖고, 람다의 리턴값이 그 아규먼트에서 파라미터가 없는 메서드 호출 결과인 경우 메서드 참조 방식으로 람다 표현식을 작성할 수 있다.
    • 람다의 아규먼트에서 메서드를 호출한 값이 람다의 리턴값인 경우
  • x -> x.toLowerCase() 와 String::toLowerCase은 같은 람다 표현식.
  • x → x.length() 와 Strign::length
  • 람다 표현식이 아규먼트를 1개만 전달 받고, 람다의 구현부가 메서드 1개 호출하여 그 메서드가 람다의 아규먼트를 전달받는 경우
  • 메서드 참조 방식을 사용할 수 있음
    • (x) → System.out.println(x)
    • System.out::println
  • ex)
  • languages.forEach(x -> System.out.println(x)); languages.forEach(System.out::println);

'programming > Java [notion 정리본 업로드]' 카테고리의 다른 글

Oracle  (1) 2024.04.15
입출력 스트림  (0) 2024.04.15
JSP/SERVLET 프로젝트 생성 시 Dependencies  (0) 2024.04.13
Collection  (0) 2024.04.13
예외처리 (Exception)  (0) 2024.04.13