[Chap 13] 13 내부 클래스, 람다식, 스트림
내부 클래스
내부 클래스란 말 그대로 클래스 안에 선언된 클래스를 의미한다. 내부 클래스에는 인스턴스 내부 클래스, 정적(static) 내부 클래스, 지역(local) 내부 클래스, 익명 내부 클래스 가 있다.
인스턴스 내부 클래스
인스턴스 내부 클래스는 인스턴스 변수를 선언할 때와 같은 위치에 선언하며, 외부 클래스 내부에서만 생성하여 사용하는 객체를 선언할 때 쓴다.
//InnerTest.java
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;
void inTest() {
System.out.println("OutClass num = " + num + "(외부 클래스의 인스턴스 변수)");
System.out.println("OutClass sNum = " + sNum + "(외부 클래스의 정적 변수)");
}
}
public void usingClass() {
inClass.inTest();
}
}
public class InnerTest {
public static void main(String[] args) {
OutClass outClass = new OutClass();
System.out.println("외부 클래스 이용하여 내부 클래스 기능 호출");
outClass.usingClass();
}
}
외부 클래스를 만들고, 내부 클래스로 선언했다. 그래서 OutClass
를 선언한다면 InClass
클래스가 불려와서 출력값을 출력할 수 있는 것이다.
- 인스턴스 내부 클래스에서 사용하는 변수와 메서드
외부 클래스에서 private 예약어로 변수 num과 정적 변수 sNum을 선언했다. 이 두 변수는 private 변수지만 외부 클래스 안에 있는 내부 클래스도 사용할 수 있음을 알 수 있다. 하지만 내부 클래스에서 정적 변수를 사용하게되면 에러가 났다. 그 이유는 인스턴스 내부 클래스는 외부 클래스를 생성한 이후에 사용해야 하는데, 클래스의 생성과 상관없이 사용할 수 있는 정적 변수는 인스턴스 내부 클래스에서는 사용할 수 없기 때문이다. 정리를 하자면, 인스턴스 내부 클래스는 외부 클래스가 먼저 생성되 이후에 사용할 수 있다. 그리고 인스턴스 내부 클래스의 메서드를 사용하기 위해서는 외부 클래스의 메서드가 호출 될 때 사용할 수 있다.
정적 내부 클래스
위에서 인스턴스 내부 클래스 같은 경우는 외부 클래스의 호출이 있어야만 사용이 가능했다. 하지만 외부 클래스의 생성과는 무관하게 사용할 수 있어야하고, 정적 변수도 사용할 수 있어야 한다면 정적 내부 클래스를 사용하면 된다.
//InnerTest.java
package innerclass;
class OutClass {
private int num = 10; //외부 클래스 private 변수
private static int sNum = 20; //외부 클래스 정적 변수
/* 인스턴스 내부 클래스 사용시 썼던 코드
private InClass inClass;
public OutClass() {
inClass = new InClass();
}
*/
static class InStaticClass {
int inNum = 100;
static int sInNum = 200;
void inTest() {
System.out.println("InStaticClass inNum = " + inNum + "(내부 클래스의 인스턴스 변수 사용)");
System.out.println("InStaticClass sInNum = " + sInNum + "(내부 클래스의 정적 변수 사용)");
System.out.println("OutClass sNum = " + sNum + "(외부 클래스의 정적 변수 사용)");
}
static void sTest() {
System.out.println("OutClass snum = " + sNum + "(외부 클래스의 정적 변수 사용)");
System.out.println("InStaticClass sInNum = " + sInNum + "(내부 클래스의 정적 변수 사용)");
}
}
}
public class InnerTest {
public static void main(String[] args) {
/*인스턴스 내부 클래스 사용시 썼던 코드
OutClass outClass = new OutClass();
System.out.println("외부 클래스 이용하여 내부 클래스 기능 호출");
outClass.usingClass();
*/
OutClass.InStaticClass sInClass = new OutClass.InStaticClass();
System.out.println("정적 내부 클래스 일반 메서드 호출");
sInClass.inTest();
System.out.println("정적 내부 클래스의 정적 메서드 호출");
sInClass.sTest();
}
}
>>> 출력 결과
정적 내부 클래스 일반 메서드 호출
InStaticClass inNum = 100(내부 클래스의 인스턴스 변수 사용)
InStaticClass sInNum = 200(내부 클래스의 정적 변수 사용)
OutClass sNum = 20(외부 클래스의 정적 변수 사용)
정적 내부 클래스의 정적 메서드 호출
OutClass snum = 20(외부 클래스의 정적 변수 사용)
InStaticClass sInNum = 200(내부 클래스의 정적 변수 사용)
정적 내부 클래스
는 외부 클래스를 호출 하지 않고도 정적 내부 클래스의 메서드를 사용할 수 있음을 보기 위해서 일부로 인스턴스 내부 클래스에서의 코드를 지우지 않았다. 데이터 형을 아예 외부 클래스.정적 내부 클래스 로 불러옴을 알 수 있다. 인스턴스 내부 클래스의 메서드를 사용하기 위해서는 외부 클래스의 호출이 필요했다. 하지만 정적 내부 클래스
의 경우 바로 내부 클래스를 불러올 수 있으므로 외부 클래스를 굳이 호출할 필요가 없게 되었다.
정적 내부 클래스 | 변수 우형 | 사용 가능 여부 |
---|---|---|
일반 메서드 void inTest() |
외부 클래스의 인스턴스 변수 외부 클래스의 정적 변수 정적 내부 클래스의 인스턴스 변수 정적 내부 클래스의 정적 변수 |
X O O O |
정적 메서드 static void inTest() |
외부 클래스의 인스턴스 변수 외부 클래스의 정적 변수 정적 내부 클래스의 인스턴스 변수 정적 내부 클래스의 정적 변수 |
X O X O |
정리를 해보자면 정적 내부 클래스에서의 일반 메서드 같은 경우는 외부 클래스의 인스턴스 변수를 제외하고 모두 사용할 수 있지만 정적 메서드의 경우는 정적 변수만 사용할 수 있음을 알 수 있다.
- 사용 시기
내부 클래스를 만들고 외부 클래스와 무관하게 다른 클래스에서도 사용하기 위해서는 정적 내부 클래스로 생성하면 된다.
지역 내부 클래스
지역 내부 클래스는 지역 변수처럼 메서드 내부에 클래스를 정의하여 사용하는 것이다. 그렇기 때문에 메서드 안에서만 사용할 수 있다. 다음은 Runnable
인터페이스를 구현하는 클래스를 지역 내부 클래스로 만든 예제 이다. Runnable
인터페이스는 반드시 run() 메서드를 구현해야 됩니다.
package innerclass;
class Outter {
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() {
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 = " + Outter.sNum + "(외부 클래스 정적 변수)");
}
}
return new MyRunnable();
}
}
public class LocalTest {
public static void main(String[] args) {
Outter out = new Outter();
Runnable runner = out.getRunnable(10);
runner.run();
}
}
>>>출력값
i = 10
num = 100
localNum = 10
outNum = 100(외부 클래스 인스턴스 변수)
Outter.sNum = 200(외부 클래스 정적 변수)
getRunnable 메서드는 리턴형이 Runnable이므로 반드시 이 메서드에서 리턴을 Ruunable 자료형의 객체로 반환해주어야 한다. Runnable 인터페이스를 구현한 MyRunnable
클래스를 구현하여서 리턴형으로 사용해주었다. 그리고 Runnable이라는 객체를 생성한 후 리턴해주어야 하므로 new 예약어를 통해서 MyRunnable 클래스를 생성한 후 반환해준다. 그 후 LocalTest
클래스(Main)을 보게 되면 먼저 외부 클래스를 호출합니다. 그런다음 Runnable 객체를 반환해주는 메서드 getRunnable 을 사용하여 생성된 객체를 runner 변수에 넣어줍니다. getRunnable 메서드에서 초기화해준 변수 num과 매개변수 i같은 경우는 지역변수이다. 이런 지역변수같은 경우는 스택 메모리에 저장이 되어 사용이 끝나면 메모리에서 사라진다. 하지만 재역 내부 클래스에서는 이런 지역 변수를 상수로 처리해서 읽어올 수 가 있다. 다만 상수로 처리했기 때문에 값 변경은 불가능하다.
익명 내부 클래스
익명 내부 클래스는 단 하나의 인터페이스 또는 단 하나의 추상 클래스를 바로 생성할 수 있다.
package innerclass;
class Outter2 {
Runnable getRunnable(int i) {
int num = 100;
return new Runnable() { //익명 내부 클래스 : 클래스 이름을 빼고 바로 클래스를 생성하는 방법
@Override
public void run() {
System.out.println(i);
System.out.println(num);
}
};
}
Runnable runner = new Runnable() { //익명 내부 클래스를 변수에 대입,,,
@Override
public void run() {
System.out.println("Runnable이 구현된 익명 클래스 변수");
}
};
}
public class AnonymousInnnerTest {
public static void main(String[] args) {
Outter2 out = new Outter2();
Runnable runnerable = out.getRunnable(10);
runnerable.run();
out.runner.run();
}
}
13 . 1 내부 클래스 정리
내부 클래스 종류 | 생성 방법 |
---|---|
인스턴스 내부 클래스 | 외부 클래스를 먼저 만든 다음 내부 클래스 생성 |
정적 내부 클래스 | 내부 클래스에 static을 붙힘, 외부 클래스.정적 내부 클래스 를 통해서 바로 내부 클래스 생성 |
지역 내부 클래스 | 인터페이스의 객체를 생성하기 위해서 외부 클래스의 메서드 데이터형을 해당 인터페이스로 구현, 외부 클래스의 메서드를 호출할 경우 생성 |
익명 내부 클래스 | 메서드를 호출할 때 생성되거나, 인터페이스 타입 변수에 대입할 때 new 예약어 사용 |
람다식
함수의 구현과 호출만으로 프로그램을 만들 수 있는 프로그래밍 방식을 함수형 프로그래밍이라고 하고, 자바에서 제공하는 함수형 프로그래밍방식을 람다식
이라고 한다.
람다식 문법
str -> {System.out.println(str);}
(str) -> {System.out.println(str);}
(int x, int y) -> {System.out.println(x + y);}
매개 변수가 하나인 경우 괄호를 생략해도 되지만 두 개 이상인 경우에는 괄호를 생략할 수 없다.
str -> System.out.println(str);
(str) -> System.out.println(str);
str -> {return str.length();}
중괄호 안의 문장이 한 문장이라면 중괄호를 생략할 수 있지만, 리턴문은 중괄호를 생략할 수 없다.
str -> str.length();
(x, y) -> x + y;
리턴문이 하나라면, return 과 {}를 생략하고 식만 쓴다.
//MyNumber.java
package lambda;
public interface MyNumber {
int getMax(int num1, int num2);
}
//TestMyNumber.java
package lambda;
public class TestMyNumber {
public static void main(String[] args) {
MyNumber max = (x, y) -> (x >= y) ? x : y;
System.out.println(max.getMax(10, 20));
}
}
객체 지향 프로그래밍 방식과 람다식 비교
- 객체 지향 프로그래밍 방식
//StringConcat.java : 인터페이스 구현하기
package lambda;
public interface StringConcat {
public void makeString(String s1, String s2);
}
//추상 메서드 구현하기 using implements
package lambda;
public class StringConcatImpl implements StringConcat {
@Override
public void makeString(String s1, String s2) {
System.out.println(s1 + ", " + s2);
}
}
//TestStringConcat.java
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);
}
}
>>> 출력값
Hello, World
makeString() 메서드를 구현하기 위해서는 StringConcat
인터페이스를 구현한 StringConcatImpl 클래스가 필요합니다. 그 클래스에 makeString() 메서드를 재정의를 한 후, 객체를 불러와서 메서드를 사용합니다.
- 람다식 방식
package lambda;
public class TestStringConcat2 {
public static void main(String[] args) {
String s1 = "Hello";
String s2 = "World";
StringConcat concat2 = (s, v) -> System.out.println(s + ", " + v); //StringConcat 인터페이스 재정의
concat2.makeString(s1, s2);
}
}
객체 프로그래밍 방식을 통해서 재정의 하는 방식은 StringConcat
인터페이스를 상속받은 클래스에서 재정의를 한 후, 그 클래스를 test 파일에서 호출하여 재정의한 메서드를 사용하였다. 하지만 람다식 방식을 활용한 경우, StringConcat
인터페이스를 test 파일에서 직접 재정의 후 사용할 수 있음을 확인할 수 있다. 이때, 람다식 방식을 활용할 수 있는 인터페이스는 그 인터페이스에 선언된 메서드가 한 개인 경우만 해당된다.
익명 객체를 생성하는 람다식
- 인터페이스형 변수에 람다식 대입하기
//람다식을 인터페이스형 변수에 대입, 인터페이스형 메서드 사용
PrintString lambdaStr = s -> System.out.println(s);
lambdaStr.showString("Hello, lambda_1");
람다식을 lambdaStr 변수에 대입하여, 인터페이스형이 메서드를 사용하고 있다.
- 매개변수로 전달하는 람다식
public static void showMyString(PrintString P) {
P.showString("Hello, lambda_2");
}
>>> 클래스 main
P.showMyString(lambdaStr); //TestLambda 클래스의 정적 메서드 호출
매개변수를 보면 현재 PrintString P = lambdaStr 인 상태이다. 이는 lambdaStr 변수는 현재 람다식이 들어가있는 인터페이스형 변수이다.
PrintString P = lambdaStr = s -> System.out.println(s);
이렇게 정리할 수 있고, 결국 인터페이스의 메서드를 사용하기 위해서는 p.showString(str) 를 사용해주면 되는 것이다.
- 반환값으로 쓰이는 람다식
public static PrintString returnString() {
return s -> System.out.println(s + " world");
}
>>>main에서 메서드 호출
PrintString reStr = returnString();
reStr.showString("hello");
반환형이 람다식이므로 인터페이스형이 리턴형이 된다. 헐 reStr 이 returnString() 메서드인 상태에서 봤을 때, 이 메서드는 람다식을 리턴하고 있다.
PrintString reStr = returnString() = s -> System.out.println(s + " world");
이 되기 대문에 결국 reStr 변수에는 람다식이 들어가 있고, 그래서 s 변수에 hello가 들어가는 것이다.
스트림
스트림은 배열, 컬렉션 등의 자료를 일관성 있게 처리할 수 있다.
스트림 연산
스트림 연산에는 크게 중간 연산과 최종 연산 두 가지가 있다.
- 중간 연산 - filter(), map()
중간 연산은 자료를 거르거나 변경하여 또 다른 자료를 내부적으로 생성한다. 함수를 수행하면서 해당 조건이나 함수에 맞는 결과를 추출해 내는 중간 역할을 한다.
- 최종 연산 - forEach(), count(), sum(), reduce()
최종 연산은 생성된 내부 자료를 소모해 가면서 연산을 수행한다. 따라서 최종 연산은 마지막에 한 번만 호출된다. 이름에서도 알 수 있듯이 최종 연산이기 때문에 한 번 연산이 수행되고 나면 더이상 연산을 수행할 수 없다.
중간 연산 - filter()
filter()는 조건을 넣고, 그 조건에 맞는 참인 경우만 추출하는 경우 사용한다.
sList.stream().filter(s -> s.length() >= 5).forEach(s -> System.out.println(s));
-
sList.stream() : 스트림 생성
-
filter(s -> s.length() >= 5) : 중간 연산
-
forEach(s -> System.out.println(s)) : 최종 연산
중간 연산 - map()
map()은 클래스가 가진 자료 중 이름만 출력하는 경우에 사용합니다.
customerList.stream().map(c -> c.getName()).forEach(s -> System.out.println(s));
-
customerList.stream() : 스트림 생성
-
map(c -> c.getName()) : 중간 연산
-
forEach(s -> System.out.println(s)) : 최종 연산
최종 연산 - forEach()
위에서 봤던 것 처럼 forEach() 같은 경우는 요소를 하나씩 꺼내는 기능을 한다.
최종 연산 - sum(), count()
배열의 요소의 합계를 구하거나, 개수를 출력하는 등의 연산을 수행한다.
스트림 사용 예시
package Stream;
import java.util.ArrayList;
import java.util.List;
public class TravelTest {
public static void main(String[] args) {
travelCustomer customerLee = new travelCustomer("이순신", 40, 100);
travelCustomer customerKim = new travelCustomer("김유신", 20, 100);
travelCustomer customerHong = new travelCustomer("홍길동", 13, 40);
List<travelCustomer> customerList = new ArrayList<>();
customerList.add(customerLee);
customerList.add(customerKim);
customerList.add(customerHong);
System.out.println("=== 고객 명단을 출력합니다. ===");
customerList.stream().map(c -> c.getName()).forEach(s -> System.out.println(s));
int total = customerList.stream().mapToInt(c -> c.getPrice()).sum();
System.out.println("총 여행 경비는 " + total + "입니다.");
System.out.println("=== 20세 이상 고객 명단 정렬하여 출력 ===");
customerList.stream().filter(c -> c.getAge() >= 20).map(c -> c.getName()).sorted().forEach(s -> System.out.println(s));
}
}
댓글남기기