다음글
Java - Effective cont.
파일 입출력 (I/O) 바이트 기반 스트림 입출력 단위가 1byte인 바이트 기반 스트림으로 InputStream, OutputStream가 있다. 스트림은 한 번만 그리고 단 방향으로만 데이터를 전송하기 때문에 입출력을 동
danc9921.tistory.com
Enum
Enum은 서로 관련이 있는 상수들의 집합이다.
Enum은 열거형, 서로 관련이 있는 것들을 모아서 번호를 매겨놓은 것을 의미한다.
enum 열거형이름 {상수명1, 상수명2, 상수명3, ...}
보통 상수명은 대문자로 작성한다.
//과일을 열거형으로 작성
enum Fruits { APPLE, STRAWBERRY, ORANGE, GRAPE, WATERMELON }
여기서 { } 안에 있는 열거 상수들은 모두 각각의 '객체'이고 위의 Fruits는 5개의 열거 객체를 가지고 있다.
열거 상수를 참조할 때에는 열거형이름.상수명 으로 표현한다. 클래스의 static변수를 참조하는 것과 동일함.
enum Fruits { APPLE, STRAWBERRY, ORANGE, GRAPE, WATERMELON }
public class EnumEx{
public class void main(String[] args){
//Fruits의 APPLE 열거 객체 주소를 Fruits 타입의 변수 fruit에 대입
Fruits fruit = Fruits.APPLE;
sout (fruit) // APPLE 출력
}
}
열거 타입은 몇 가지로 한정된 값을 가지고 사용하는 데이터 타입이기 때문에, switch-case문처럼 경우를 나눠야 할 때 많이 사용된다.
enum Fruits {APPLE, STRAWBERRY, MANGO, WATERMELON, ORANGE}
public class enum_prac {
public static void main(String[] args) {
//열거 객체 메서드 values()를 사용해 enum객체를 배열로 바꿔 fruit에 주소 대입
Fruits[] fruits = Fruits.values();
for (Fruits f : fruits) {
System.out.println(f);
// 망고 0을 기준 APPLE -2, STRAWBERRY -1, WATERMELON 1, ORANGE 2
System.out.println(f.compareTo(Fruits.MANGO));
}
switch (Fruits.APPLE) { //switch-case로 많이 쓴다.
case APPLE:
System.out.println("Wow APPLE!");
break;
case WATERMELON:
System.out.println("Oh Watermelon!");
break;
case ORANGE:
System.out.println("umm..Orange..");
break;
}
}
}
APPLE
-2
STRAWBERRY
-1
MANGO
0
WATERMELON
1
ORANGE
2
Wow APPLE!
열거 객체 메서드
리턴타입 | 메서드 (매개 변수) | 특징 |
String | name( ) | 열거 객체가 갖는 문자열을 리턴한다. 리턴되는 문자열의 이름 == 열거타입을 정의할 때 사용한 상수이름 |
int | ordinal( ) | 열거 객체의 순서(0부터 시작)을 리턴한다 |
int | compareTo(비교할 값) | 주어진 비교값과 비교해서 순번 차이를 리턴한다 |
enum 타입 | valueOf(String name) | 주어진 문자열(name)의 열거 객체를 리턴한다 |
enum 타입 | values( ) | 모든 열거 객체를 배열로 리턴한다. |
public static final
JDK 1.5 이전에는 enum이 없어 여러 상수를 정의해서 쓰기 위해 public static final을 통해 상수를 전역 변수로써 지정하여 사용했다. 그리고 상수명에 숫자 값을 대입하여 구분해 사용했다.
하지만 다른 범주에 있는 상수를 정의하는 경우, 상수명이 중복되는 경우가 생길 수 있는데 이러면 컴파일 에러가 발생한다. 이를 해결하기 위해 인터페이스를 통해 상수를 구분할 수는 있지만 오직 상수에 순서를 매기기 위해 대입한 숫자 값으로 인해 개념 자체가 비교 대상이 아닌 상수인데도 == 를 통해서 비교가 가능해져 버린다.
이 문제는 각각의 객체로 만들면 해결 가능 하지만 코드가 너무 길어지는 단점이 있다. 또한 switch 문을 사용할 수 없다.
이런 단점을 커버하기 위해 enum이 탄생한 것이다.
enum의 장점
1. 상수의 중복을 피할 수 있다.
2. 타입에 대한 안정성 보장
3. 코드가 간결해지고 가독성 향상
4. switch-case 문에 사용 가능
애너테이션 (Annotation)
애너테이션은 '주석, 주해, 메모'라는 뜻을 갖고 있다. //나 /* */을 이용한 주석과 같은 기능을 수행하며 주석과 마찬가지로 코드에는 아무 영향을 끼치지 않고 정보를 전달한다.
애너테이션과 // /**/ 주석의 차이점은 '정보의 제공 대상'으로 구분할 수 있는데 // /**/의 주석은 코드를 읽는 사용자에게 정보를 제공하고, 애너테이션은 프로그램에게 정보를 제공한다.
예를 들어 지금까지 간혹 가다가 IDE에서 오버 라이딩을 할 때 @Override라는 태그가 위에 붙은 적이 있었는데 이게 바로 애너테이션이다.
class LowerClass extends higherClass
{
@Override
void overrideThisMethod(){...}
}
위의 코드는 컴파일러가 같은 이름의 메서드가 상위 클래스에 있는지 확인하고 없으면 에러 메시지를 출력한다.
애너테이션은 해당 프로그램에 미리 정의된 종류와 형식으로 작성해야 한다. 아래의 @Test는 테스트 프로그램에게 아래의 메서드를 검사하라고 알릴 뿐, 테스트 프로그램을 제외한 다른 프로그램에게는 아무런 의미가 없다.
@Test //아래의 메서드가 테스트 대상임을 테스트 프로그램에게 알린다.
public void method() { ... }
즉 애너테이션의 용도를 정리해보면
1. 컴파일러에게 문법 에러를 체크하도록 정보를 제공한다.
2. 프로그램 빌드 시에, 코드를 자동으로 생성할 수 있도록 정보를 제공한다.
3. 런타임에 특정 기능을 실행할 수 있도록 정보를 제공한다.
애너테이션의 종류
애너테이션은 자바에서 기본적으로 제공하는 표준 애너테이션과, 애너테이션을 정의하는데 쓰이는 메타 애너테이션이 있다.
표준 애너테이션
표준 애너테이션 | 특징 |
@Override | 컴파일러에게 overriding하는 메서드라는 것을 알림 |
@Deprecated | 앞으로 사용하지 않을 것을 권장하는 대상임을 알림 |
@FunctionalInterface | 함수형 인터페이스 라는것을 알림 (JDK1.8) |
@SuppressWarning | 컴파일러가 경고메세지를 나타내지 않음 |
@Override
메서드 앞에만 붙일 수 있는 애너테이션으로, 상위 클래스의 메서드를 오버 라이딩하는 것이라 컴파일러에게 알린다. 만약 해당 메서드가 상위 클래스에 존재하지 않는다면 컴파일 에러를 발생시킨다.
Overriding을 할 때 @Override 애너테이션을 사용하는 것을 습관화 하자. 만약 메서드의 이름이 다른데 오버 라이딩을 하게 되면 컴파일러는 그냥 새로운 메서드로 인식하여 에러를 발생시키지 않는다. 하지만 실행 후 런타임에 에러가 발생하거나 결과가 예상과 다를 수 있는데 이 시점에서는 어디에서 뭐가 잘못되었는지 파악하기 매우 어렵다.
따라서 오타 등의 실수를 사전에 방지할 수 있으므로 잘 참고하자.
@Deprecated
새로운 버전의 JDK에서 새로운 기능이 추가되거나 기존의 기능들을 개선하는 경우 더 이상 사용하지 않는 필드나 메서드가 생길 수 있다. 이런 사용하지 않는 필드나 메서드에 @Deprecated를 사용해 컴파일러에게 '호환성 문제를 고려해 삭제하지는 않았지만 사용하지 않을 것을 권장한다.'라고 알린다.
@SuppressWarnings
선언한 곳의 컴파일 경고를 나타나지 않게 한다. 따라서 경고가 날 것을 알면서 무시해야 할 때 쓰인다. 뒤에 괄호를 만들어 특정 메시지를 지정할 수도 있다.
주로 사용되는 것은 deprecation, unchecked, rawtypes, varargs 정도이다. JDK의 버전에 따라 계속 추가된다.
@SuppressWarnings("all") | 모든 경고 억제 |
@SuppressWarnings("deprecation") | deprecation 메서드를 사용한 경우 경고 억제 |
@SuppressWarnings("fallthrough") | switch-case에서 break가 없을때 경고 억제 |
@SuppressWarnings("null") | null 관련 경고 억제 |
@SuppressWarnings("finally") | finally 관련 경고 억제 |
@SuppressWarnings("unchecked") | 비 검증된 연산자 관련 경고 억제 |
@SuppressWarnings("rawtypes") | 지네릭 타입 지정하지 않았을때 발생하는 경고 억제 |
@SuppressWarnings("unused") | 사용하지 않는 코드 관련 경고 억제 |
@SuppressWarnings("varargs") | 가변 인자의 타입이 지네릭 타입일때 나오는 경고 억제 |
@FunctionalInterface
함수형 인터페이스를 사용할 때, 올바르게 선언했는지 확인해준다. 애너테이션이 없어도 작성이 가능하지만.
@Override처럼 실수를 방지하기 위한 확인용 애너테이션이다. 함수형 인터페이스는 하나의 추상 메서드만 가지고 있어야 하는데 추상 메서드가 2개 이상일 경우 에러를 일으킨다.
@FunctionalInterface
public interface Cleanning {
public abstract void vacuum ();
}
메타 애너테이션
메타 애너테이션은 애너테이션에 붙이는 애너테이션이며, 애너테이션의 적용 대상이나 적용 범위를 정하는 등, 애너테이션을 정의하는 데 사용한다.
메타 애너테이션 | 특징 |
@Target | 애너테이션이 적용 가능한 대상을 지정하는데 사용 |
@Documented | 애너테이션 정보를 javadoc으로 작성된 문서에 포함 시킨다 |
@Inherited | 애터테이션이 하위 클래스에 상속 되도록 한다 |
@Retention | 애너테이션이 유지되는 기간를 정하는데 사용 |
@Repeatable | 애너테이션을 반복해서 적용할 수 있게 한다 (JDK1.8) |
@Target
애너테이션이 적용 가능한 대상을 지정하는 데 사용된다. @Target으로 지정할 수 있는 애너테이션 적용 대상의 종류는 아래와 같다. 이 종류들은 "java.lang.annotation.ElementType"에 정의되어 있다.
대상 타입 | 적용 범위 |
ANNOTATION_TYPE | 애너테이션 |
CONSTRUCTOR | 생성자 |
FIELD | 필드 (멤버변수, enum 상수) |
LOCAL_VARIABLE | 지역변수 |
METHOD | 메서드 |
PACKAGE | 패키지 |
PARAMETER | 매개변수 |
TYPE | 타입 (클래스, 인터페이스 , enum) |
TYPE_PARAMETER | 타입 매개변수(JDK1.8) |
TYPE_USE | 타입이 사용되는 모든 곳(JDK1.8) |
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
//static import문을 이용하여 ElementType.TYPE 대신 TYPE과 같이 간단히 작성할 수 있습니다.
@Target({FIELD, TYPE, TYPE_USE}) // 적용대상이 FIELD, TYPE, TYPE_USE
@interface
MyAnnotation { } // MyAnnotation을 정의
@MyAnnotation // 적용대상 TYPE
class Main {
@MyAnnotation // 적용대상 FIELD
int i;
}
@Documented
애너테이션에 대한 정보가 javadoc 문서에 표기된다. @Override와 @SuppressWarnings를 제외하고 모두 @Documented가 적용되어있다.
@Inherited
애너테이션이 하위 클래스에 상속되도록 한다. @Inherited가 붙은 애터 네이션을 상위 클래스에 붙이면, 하위 클래스도 이 애너테이션이 붙은 것과 동일하게 인식된다.
@Retention
애너테이션이 유지(retention) 되는 기간을 지정할 때 사용한다.
애너테이션의 유지 정책 (retention policy) | |
유지 정책 | 뜻 |
SOURCE | 소스 파일에만 존재. 클래스 파일에는 존재 X |
CLASS | 클래스 파일에 존재. 실행시에 사용 불가. 기본값 |
RUNTIME | 클래스 파일에 존재. 실행시에 사용 가능 |
@Repeatable
보통 하나의 대상에 한 종류의 애너테이션을 붙이지만, @Repeatable을 붙이면 애너테이션을 여러 번 붙일 수 있다.
사용자 정의 애너테이션
정해진 애너테이션을 쓸 수도 있지만 내가 직접 애너테이션을 정의해서 사용하는 것도 가능하다.
정의하는 방법은 인터페이스를 정의하는 것과 비슷하고, 애너테이션은 java.lang.annotation 인터페이스를 상속받았기 때문에 다른 클래스나 인터페이스를 상속받을 수는 없다.
@interface 애너테이션_이름 { //인터페이스 앞에 @를 붙이면 애너테이션 정의 가능
타입 요소명(); //애너테이션 요소 선언
}
애너테이션의 요소
애너테이션 안에 선언된 메서드를 '애너테이션의 요소(element)'라고 한다.
요소는 매개변수는 없지만 반환 값이 있는 추상 메서드의 형태와 동일하며 상속을 통해 구현할 필요가 없다. 하지만 애너테이션을 적용할 때 이 요소들의 값을 전부 지정해야만 한다.
각 요소들은 기본값을 가질 수 있다. 기본값이 있는 요소를 설정하면, 애너테이션을 적용할 때 값을 지정하지 않으면 기본값이 들어간다. (기본값은 null을 제외한 모든 리터럴 사용 가능)
람다식 p794
함수형 프로그래밍은 병렬 처리와 이벤트 지향 프로그래밍에 적합하고 최근 각광받는 프로그래밍 기법이다.
자바에서는 이런 함수형 프로그래밍을 객체지향 프로그래밍과 같이 사용함으로써 더욱 효율적인 프로그래밍을 할 수 있도록 람다식 (Lambda Expressions)을 지원한다. 이로 인해 기존의 코드 패턴들이 많이 변화하였다.
람다식을 간단하게 표현하자면 메서드를 하나의 '식 (expression)'으로 표현한 것이다 (수학의 그 '식'이다). 메서드를 람다식으로 표현하면 메서드의 이름과 반환 값이 없어지게 돼서 익명 함수(anonymous function)라고도 한다.
람다식을 통해 코드가 매우 간결해지고, 컬렉션의 요소를 필터링하거나 매핑해서 원하는 결과를 쉽게 얻을 수 있다.
(매개변수) -> {실행코드}의 형태로 작성된다
반환타입 메서드이름 (매개변수 선언) {
......
문장들
......
}
//위 식을 람다식으로 변환하면 아래처럼 표현 할 수 있다.
(매개변수 선언) -> { ... 문장들 ... }
// -> 는 매개변수( ) 를 이용해 { } 안의 실행문을 실행하라는 명령어라고 이해하자.
// 매개변수에 타입은 런타임에 대입되는 값에 따라서 자동으로 추론된다. 따라서 일반적으로
// 람다식에서는 매개변수의 타입을 설정하지 않는다.
// (중요!) 매개변수가 없다면 빈 괄호 ( )를 꼭 써야한다. ( ) -> { 실행될 문장 }
// 1. 기본 작성
(타입 매개변수) -> { ... }
// 2. 매개변수가 1개일 경우, 매개변수의 ( ) 생략 가능
매개변수 -> { ... }
// 3. 매개변수가 2개 이상이고, 리턴문만 존재할 경우 return 생략 가능
(매개변수1, 매개변수2) -> 리턴값;
(n1, n2) -> {return n1 * n2} // return문만 존재하므로
..
(n1, n2) -> n1 * n2 //return 생략 가능, 중괄호도 생략
// 4. 매개변수가 2개 이상, 실행문을 실행하고 결과값을 리턴할 경우
(매개변수1, 매개변수2) -> { ... } ;
함수형 인터페이스 p797
자바에서는 새로운 함수형 프로그래밍을 위해 새로운 함수 문법을 정의하는 대신에, 인터페이스의 문법을 이용하여 람다식을 표현했다.
단 하나의 추상 메서드만 포함하는 인터페이스를 함수형 인터페이스 라고 한다.
타깃 타입 + 함수형 인터페이스
람다식을 표현하기 위해 사용되는 함수형 인터페이스는 단 하나의 추상 메서드만 포함할 수 있다. 함수형 인터페이스를 구현할 때, 두 개 이상의 추상 메서드가 선언되지 않도록 애너테이션 @FunctionalInterface를 인터페이스 선언 시에 사용한다.
@FunctionalInterface 애너테이션은 선택사항이지만, 만약의 실수를 방지할 수 있으니 가능하면 붙이자.
아래의 예제 코드는 추상 메서드의 반환 타입 여부, 매개변수의 개수에 따른 다른 작성법을 연습한 것이다.
@FunctionalInterface
interface MyFunctionalInterface {
public void accept(int x); //매개변수 O / 반환타입 X
//public void accept2(int z); - 애너테이션으로 2개의 추상 메서드 방지
//ERROR:: Multiple non-overriding abstract methods found
//in interface Effective.MyFunctionalInterface
}
@FunctionalInterface
interface FuncInterfaceTest {
public int calculation(int x, int y); //매개변수 O / 반환타입 O
}
public class Lambda_prac {
public static void main(String[] args) {
MyFunctionalInterface exam;
exam = (x) -> {
int result = x * 5;
System.out.println(result);
};
exam.accept(5); // 25
exam = (x) -> System.out.println(x * 5);
exam.accept(2); // 10
//////////////////////////////////
FuncInterfaceTest caltest;
caltest = (x, y) -> {
int result = x + y;
return result;
};
System.out.println(caltest.calculation(2, 5)); // 7
caltest = (x, y) -> x / y;
double result2 = caltest.calculation(5, 2);
System.out.println(result2); // 2.0
}
}
메서드 레퍼런스 p812
메서드 레퍼런스는 람다식을 사용할 때 불필요한 매개변수를 생략하는 것에 목적이 있다. 람다식은 기존에 있는 메서드를 그냥 호출만 하는 경우가 종종 있다.
// 두개의 값을 받아 큰수를 리턴하는 Math.max( ) 호출하는 람다식
(i, j) -> Math.max(i, j);
람다식을 이용하면 단순하게 두 개의 인자를 매개변수 값으로 전달하는 역할만 하기에 불편해 보일 수 있다. 이럴 때
메서드 레퍼런스를 이용하면 깔끔하게 처리가 된다.
Math::max;
메서드 레퍼런스도 람다식처럼 인터페이스의 익명 구현 객체로써 생성된다. 따라서 인터페이스 내부의 추상 메서드가 리턴 타입이 무엇인지, 어떤 매개변수를 인자로 받는지에 따라 달라진다.
메서드 레퍼런스는 static 또는 인스턴스 메서드를 참조할 수 있고 생성자의 참조도 가능하다.
람다식을 메서드 참조로 표현하기
종류 | 람다식 | 메서드 참조 |
static(정적) 메서드 참조 | (x) -> ClassName.method(x) | ClassName::method |
인스턴스 메서드 참조 | (obj.x) -> obj.method(x) | ClassName::method |
특정 개체 인스턴스 메서드 참조 | (x) -> obj.method(x) | obj::method |
하나의 메서드만 호출하는 람다식은
'클래스이름::메서드이름' 또는 '참조변수::메서드이름'
으로 표현할 수 있다.
생성자 참조
생성자를 호출하는 람다식도 메서드 참조로 바꿔 표현할 수 있다. 생성자 참조라는 건 객체를 생성한다는 것을 의미한다.
// 생성자 매개변수가 없을 시
Supplier<MyClass> sup = () -> new MyClass(); //람다식 표현
Supplier<MyClass> sup = MyClass::new; //메서드 레퍼런스
//생성자 매개변수가 있을 시
// 1개의 매개변수
Function<Integer, MyClass> f = (i) -> new MyClass(i); // 람다식
Function<Integer, MyClass> f1 = MyClass::new; // 메서드 레퍼런스
==========================================================================
// 2개의 매개변수
DoubleFunction<Interger, Integer, MyClass> df = (i,j) -> new MyClass(i,j); //람다식
DoubleFunction<Interger, Integer, MyClass> df1 = MyClass::new; //메서드 레퍼런스
//배열을 생성할 시
ArrayFunc<Interger, int[]> af = x -> new int[]; //람다식
ArrayFunc<Integer, int[]> af1 = int[]::new; //메서드 레퍼런스
메서드 참조는 마치 람다식을 static 변수처럼 사용할 수 있게 도와준다. 예시 코드를 따라 적어보면서도 확실히 간략화된 람다식에서 더 간략화되었다는 것을 체감할 수 있었다. 편하다. 다만 아직은 어색하고 어려운 것 같다.
스트림 (Stream) p814
스트림(Stream)은 배열이나 컬렉션의 데이터 저장 요소를 하나씩 참조하여 람다식으로 처리할 수 있도록 도와주는 반복자이다.
스트림은 데이터 소스를 추상화하고 데이터를 관리하는데 자주 쓰이는 메서드들을 정의해 놓았다. 즉 데이터 소스가 무엇이든 간에 같은 방식으로 다룰 수 있게 되었고 코드의 재 사용성이 높아진다는 것을 의미한다.
스트림 사용 시 주의사항
- Stream은 Read-only이다. 데이터 소스의 값 변경 X
- Stream은 1회용이다. 한번 사용하면 없어지므로 필요하면 다시 스트림을 만들어야 한다.
java.util.stream (Java SE 11 & JDK 11 )
Classes to support functional-style operations on streams of elements, such as map-reduce transformations on collections. For example: int sum = widgets.stream() .filter(b -> b.getColor() == RED) .mapToInt(b -> b.getWeight()) .sum(); Here we use widgets, a
docs.oracle.com
스트림의 특징
1. 선언형으로 데이터 소스를 처리한다.
'HOW?'에 중점을 두었던 명령형 프로그래밍에서는 순서대로 진행되어야 코드를 이해할 수 있지만, 'WHAT?'에 중점을 두는 선언형 프로그래밍의 경우, 내부가 어떤 식으로 돌아가는지의 원리를 모르고 있어도 코드가 어떤 일을 수행하는지 이해할 수 있다.
즉 추상화를 활용하며, 간결하고 가독성 높은 프로그래밍을 할 수 있다.
2. 람다식으로 요소 처리 코드를 제공한다.
Stream이 포함한 대부분의 요소 처리 메서드는 함수형 인터페이스를 매개 타입으로 갖고 있기 때문에, 람다식과 메서드 참조를 이용하여 요소 처리 내용을 파라미터로 전달할 수 있다.
3. 내부 반복자를 이용하기 때문에 병렬 처리가 쉽다.
스트림을 이용한 작업이 간단한 이유는 스트림 내부에서 진행되는 내부 반복이다. 예를 들어 스트림에 정의된 메서드 forEach( )는 매개변수로 대입된 람다식을 데이터 소스의 모든 요소에 적용시킨다. 즉 메서드 안으로 for문을 넣은 것과 같고 수행할 작업을 매개변수로 받는다)
for (String str : strList) ------------------> stream.forEach(System.out::println);
System.out.println(str);
4. 중간 / 최종 연산을 할 수 있다.
스트림이 제공하는 연산은 컬렉션의 요소에 대해 중간 연산과 최종 연산으로 나눌 수 있다.
연산의 더 자세한 내용은 아래 부분에 있다.
stream.distinct().limit(5).sorted().forEach(System.out::println)
// --------- -------- ------- ----------------------------
// 중간연산 중간연산 중간연산 최종연산
중간 연산
연산 결과가 스트림인 연산. 스트림에 연속해서 중간 연산할 수 있다.
매핑, 필터링, 정렬을 수행한다.
모든 중간 연산의 결과는 스트림이지만, 결과로 나온 스트림은 연산 전 스트림과 다르다.
최종 연산
연산 결과가 스트림이 아닌 연산. 스트림의 요소를 소모하므로 한 번만 시행 가능하며, 더 이상 사용 불가
반복, 카운팅, 평균, 총합 등의 집계를 수행한다.
스트림 파이프라인
다량의 데이터를 가공하여 축소하는 것을 리덕션(reduction)이라 한다. 데이터의 합계, 평균값, 카운팅, 최소 최댓값 등이 대표적인 리덕션의 결과물이다. 그러나 컬렉션의 요소를 바로 리덕션을 통해 가공할 수 없을 때에는 스트림을 통하여 필터링, 매핑, 정렬, 그룹핑 등의 중간 연산이 필요하다.
stream.distinct().limit(5).sorted().forEach(System.out::println)
// --------- -------- ------- ----------------------------
// 중간연산 중간연산 중간연산 최종연산
위의 코드처럼 스트림을 통해 여러 번의 중간 연산을 거친 후에, forEach로 최종 연산을 하여 출력하는 것을 알 수 있다.
필요한 스트림 메서드를 이용하여 주어진 데이터를 맞게 가공하는 과정인 중간 연산 ->
가공하고 남은 결과를 최종 연산으로 반환한다.
이처럼 최종 연산을 위해 여러 번의 스트림이 연결되어 있는 구조를 보이는데 이를 파이프라인이라 한다.
한 가지 중요한 포인트는, 최종 연산이 수행되기 전 까지는 중간 연산은 수행되지 않는다. 메서드를 사용했을 때 즉각적으로 연산이 수행되는 것이 아닌, 어떤 작업이 수행될 것인지 지정만 해놓고 최종 연산이 실행될 때 비로소 중간 연산을 통해 스트림의 요소가 소모되고 최종적으로 최종 연산에서 전부 소모된다. 그리고 스트림은 사라진다. (일회성)
스트림 생성
당연한 얘기지만 스트림을 이용하기 위해선 스트림을 생성해야 한다.
Collection 인터페이스에 stream( )이 미리 정의가 되어있기 때문에 Collection 인터페이스를 구현한 하위 컬렉션 클래스 (List, Set 등) 들은 모두 stream ( ) 메서드로 해당 컬렉션을 소스로 하는 스트림을 만들 수 있다.
스트림의 데이터 소스가 될 수 있는 대상은 컬렉션, 배열, 원시 자료형 그리고 특수한 종류의 스트림
(IntStream, LongStream, DoubleStream) 등이 있다.
컬렉션 소스
// 기본 생성 형식
Stream<T> Collection.stream()
//List로부터 Stream 생성
List<Integer> list = Arrays.asList(1, 3, 5, 7, 9); // 가변 인자
Stream<Integer> intStream = list.stream(); // List를 소스로 하는 컬렉션 생성
intStream.forEach(System.out :: println); // 스트림의 모든 요소 출력
intStream.forEach(System.out :: println); // ERROR! 스트림이 이미 닫혔다.
배열 소스
배열의 원소를 스트림의 소스로 사용하기 위해서는 Stream의 of( ) 메서드, Arrays의 stream( ) 메서드를 사용한다.
// 기본 생성 형식
Stream<T> Stream.of(T...values)
Stream<T> Stream.of(T[])
Stream<T> Arrays.stream(T[]);
Stream<T> Arrays.stream(T[] array, int x, int y)
// 문자열 (String) 스트림
Stream<String> strStream = Stream.of("a", "b", "c"); // 가변 인자
Stream<String> strStream = Stream.of(new String[] {"a", "b", "c" } );
Stream<String> strStream = Arrays.stream(new String[] {"a", "b", "c"} );
Stream<String> strStream = Arrays.stream(new String[] {"a", "b", "c"}, 1, 3);
// int, long, double 같은 기본형 배열을 소스로 하는 스트림
IntStream IntStream.of(int ... values)
IntStream IntStream.of(int[])
IntStream Arrays.stream(int[])
IntStream Arrays.stream(int[] arr, int x, int y)
int형 = IntStream / IntStream.of
long형 = LongStream / LongStream.of
double형 = DoubleStream / DoubleStream.of
특정 범위의 정수 소스
*IntStream & LongStream은 range( ) / rangeClosed( ) 함수를 이용해 for문을 대체 할 수 있다.
*지정된 범위의 연속된 정수를 스트림으로 생성해서 반환한다.
IntStream stream = IntStream.range(1, 5) // 1, 2, 3, 4 (마지막 포함 X)
LongStream Lstream = LongStream.rangeClosed(1, 5) // 1, 2, 3, 4, 5 (마지막 포함 O)
중간 연산
= Intermediate Operations
중간 연산은 연산 결과를 스트림으로 반환하기 때문에, 여러 번 수행이 가능하다.
주요 중간 연산 메서드 (핵심 - map( ), flatMap( ))
필터링 filter( ), distinct( )
filter( ) | 주어진 조건에 맞는 데이터만 골라내서 더 작은 컬렉션으로 만든다. 매개변수로 조건(Precicate)가 주어지고 참일 경우에만 필터링 한다. |
distinct( ) | Stream에서 중복된 요소를 제거한다. |
// 기본 형식
Stream<T> filter(Predicate<? super T> predicate)
Stream<T> distinct( )
// distinct( ) 의 사용 예
IntStream intStream = IntStream.of(1,2,2,3,3,4,4,5,6,6);
IntStream.distinct( ).forEach(System.out :: print); // 123456
// filter( ) 사용 예
IntStream intStream = IntStream.range(1, 11); // 1~10
intStream.filter(i -> i%2 ==0).forEach(System.out::print) // 246810
변환 / 매핑 map( )
스트림의 요소에 저장된 저장된 값을 특정한 형태로 변환해야 하거나 원하는 필드만 뽑아내고 싶을 때 map( ) 메서드를 사용한다. 예를 들어. String을 요소로 같은 스트림을 모두 소문자나 대문자 String의 요소로 바꾸고자 할 때 map을 사용할 수 있다.
map( ) 이외에도 mapToInt( ), mapToLong( ), mapToDouble( )등의 메서드가 있다.
위의 3개의 mapOOO( ) 메서드는 일반적인 Stream타입 객체를 원시(raw) 타입의 Stream으로 바꾸거나 그 반대의 경우에 사용한다. 반대로 원시 객체는 mapToObject를 통해 일반Stream타입 객체로 바꿀 수 있다.
// 메서드의 선언
// 매개변수로 T 타입을 R 타입으로 변환해서 반환하는 함수를 지정해야 한다.
Stream<R> map(Function<? super T, ? extends R> mapper)
import java.util.*;
public class stream_prac {
public static void main(String[] args) {
List<String> animals = Arrays.asList("Cat", "Dog", "horse", "hamster");
animals.stream()
.map(i -> i.toLowerCase()) //전부 소문자
.forEach(System.out::println);
System.out.println("\n");
animals.stream() //위의 스트림이 닫혔으니 다시 스트림 생성
.map(s -> s.toUpperCase()) // 전부 대문자
.forEach(System.out::println);
}
}
flatMap( )
flatMap( )메서드는 여러 개의 스트림을 1차원으로 평면화 된 하나의 스트림으로 만들어 주는 역할을 한다.
아래의 그림을 보면 직관적으로 알 수 있다.
문자열을 합치고자 하는데 map( ) 메서드를 사용했을 때의 결괏값은 Stream <Stream <String>>으로 스트림 안에 스트림이 들어간 구조가 되어버렸다. [Laptop, Phone, Mouse, Keyboard]을 얻고 싶은 건데
[[Laptop, Phone] , [Mouse, Keyboard]] 이 튀어나 봐 버렸다.
이럴 경우에 flatMap( ) 메서드를 사용하면 된다.
정렬 sorted( )
스트림을 정렬할 때에는 sorted( ) 메서드를 사용한다.
//기본 형식
Stream<T> sorted();
Stream<T> sorted(Comparator<? super T> comparator)
sorted( ) 메서드는 지정된 Comparator를 매개변수로 하여 스트림을 정렬한다.
매개변수 없이 호출할 경우 오름차순으로 정렬을 하게 된다. 이는 스트림 요소의 기본 정렬 기준이 Comparable 이기 때문인데, 따라서 스트림의 요소가 Comparable을 구현한 클래스가 아니면 예외가 발생한다.
내림차순으로 정렬하려면 Comparator의 reverseOrder( ) 메서드를 사용한다.
Comparator를 매개변수로 하는 대신 int값을 반환하는 람다식을 사용하는 것도 가능하다.
**예제 넣기
조회 / 연산 결과 확인 peek( )
연산과 연산 사이에 올바르게 처리되었는지 확인하고 싶을 때 peek( ) 메서드를 사용한다. 요소를 순회하며 출력하는 부분에서 forEach( ) 메서드와 공통점이 있지만 peek( )은 중간 연산 메서드라 스트림의 요소를 소모하지 않는다. 스트림 내부에서 여러 번 사용이 가능하다.
디버깅을 할 때 주로 사용하며 특히 filter( )나 map( )의 중간 연산 결과를 확인할 때 유용하게 쓸 수 있다.
최종 연산
= Terminal Operation
최종 연산은 연산 결과가 스트림이 아니기 때문에, 단 한 번만 연산이 가능하다. 최종 연산의 결과물은 스트림 요소의 합과 같은 단일 값일 수도 있고, 스트림의 요소가 담긴 배열 또는 컬렉션일 수 있다.
핵심 메서드: reduce( ), collect( )
연산 결과 확인 forEach( )
위의 예시에서 자주 쓰인 forEach( )는 최종 연산자로 파이프라인 마지막에서 요소를 한 번씩 연산한다. forEach의 값을 리턴할 때도 사용하지만 이메일 발송, 스케쥴링 등 리턴 값이 없는 작업에도 많으 쓰인다.
void forEach(Consumer<? super T> action)
IntStream
.filter(num -> num %2 ==0)
.forEach(System.out::println); // or (i -> System.out.println(i));
조건 검사 / 매칭 match( )
Stream의 요소에 대해 특정 조건을 만족하는지 검사하고 싶을 때 match( ) 메서드를 사용할 수 있다. match ( ) 메서드는 함수형 인터페이스인 Predicate를 받아 조건을 만족하는지 검사하고, 연산 결과를 boolean 타입으로 반환한다.
allMatch( ) | 모든 요소들이 매개변수 Predicate의 조건을 만족하는지 검사 |
anyMatch( ) | 적어도 한 개의 요소가 매개변수 Predicate의 조건을 만족하는지 검사 |
noneMatch( ) | 모든 요소들이 매개변수 Predicate의 조건을 만족하지 않는지 검사 |
통계 sum( ), count( ), average( ), max( ), min( )
IntStream 같은 기본형 스트림에는 요소들에 대한 통계 & 집계 정보를 얻을 수 있는 메서드들이 있다.
기본형 스트림이 아닌 경우에는 통계와 관련된 메서드는 count, max, min 3개만 존재한다.
리듀싱 reduce( )
reduce( ) 메서드는 스트림의 요소를 줄여 나가면서 연산을 하고 최종 결과를 리턴한다.
앞의 두 요소의 연산 결과를 가지고 그다음 요소와 연산한다.
reduce( ) 메서드는 최대 3개의 매개변수를 전달받을 수 있다.
매개 변수 | 의미 |
Accumulator | 각 요소를 계산한 중간 결과를 생성하기 위해 사용 (누적이라는 의미를 생각해보자) |
Identity | 연산을 위한 초기값 |
Combiner | 병렬 스트림(Parlallel Stream)에서 나누어 계산된 결과를 하나로 합칠때 사용 |
Collect( )
collect( ) 메서드는 Stream의 요소를 어떤 식으로 수집할 것인가를 정의한 Collector타입을 인자로 받고 이는 Collector 인터페이스를 구현한 클래스이다.
따라서 Stream의 요소들을 List나 Set, Map 등 다른 종류의 결과로 수집하고 싶을 때 사용할 수 있다.
일반적으로 List로 Stream의 요소를 수집하는 경우가 많다. 자주 사용하는 작업으로 Collectors 객체에서 static 메서드로 제공하고 있고, 원하는 것이 없어도 직접 Collector 인터페이스를 구현해 사용할 수 있다.
용어가 헷갈리니 잘 숙지해두도록 하자
collect( ) - 스트림의 최종 연산. 매개변수로 Collector 타입을 받는다.
Collector - 인터페이스. 컬렉터는 이 인터페이스를 구현해야 한다.
Collectors - 클래스. static 메서드로 미리 작성된 컬렉터를 제공한다.
collect( ) 메서드는 매개변수의 타입으로 Collector를 받는데 이는 매개변수의 타입이 Collector 인터페이스를 구현한 Collectors 클래스의 객체 여야 한다는 의미이다.
그리고 collect( ) 메서드는 이 객체에 구현된 방법대로 스트림의 요소를 처리한다.
(sort( )할 때 Comparator가 필요하듯이, collect( )할 때는 Collector가 필요하다.)
Collectors의 메서드 종류는 아래에서 확인하자.
Collectors (Java Platform SE 8 )
Returns a Collector implementing a "group by" operation on input elements of type T, grouping elements according to a classification function, and returning the results in a Map. The classification function maps elements to some key type K. The collector p
docs.oracle.com
중간/최종 연산 리스트
Optional <T> p835
Optional <T>는 지네릭 클래스로 'T타입의 객체'를 감싸는 래퍼 클래스이다. Optional은 많은 개발자들을 괴롭히는 NullPointerException (NPE), 즉 null 값으로 인해 에러가 발생하는 현상을 객체 차원에서 방지할 수 있다.
public final class Optional<T>{
private final T value; //T 타입의 참조 변수\
...
}
최종 연산의 결과를 그냥 반환하는 것이 아닌, Optional 객체에 담아서 반환하는 것이 요지인데, 이렇게 객체에 담아 반환을 하면, 반환 결과가 null인지 매번 if문으로 체크할 필요가 없어진다. 그냥 Optional에 정의된 메서드를 통해 간단히 처리가 가능해지는 것이다.
즉 더 간결하고 안전한 코드 작성이 가능해진다.
Optional 객체 생성
Optional의 객체를 생성하기 위해서는 of( ) 또는 ofNullable( )을 사용한다.
String str = "abcd";
Optional<String> optVal = Optional.of("abcd");
Optional<String> optVal = Optional.of(str);
Optional<String> optVal = Optional.of(new String("abcd");
만약 참조 변수의 값이 null일 가능성이 있다면 of( ) 메서드 대신 ofNullable( ) 메서드를 사용해야 한다.
of( )는 매개변수에 null값이 들어오면 NPE 에러를 발생시킨다.
Optional<String> optVal = Optional.of(null); // NullPointerException ERROR!
Optional<String> optVal = Optional.ofNullable(null); // OK
Optional타입의 참조 변수를 기본값으로 초기화하려면 empty( ) 메서드를 사용한다.
Optional<String> optVal = Optional.<String>empty(); // 빈 객체로 초기화
Optional<String> optVal = null; // null로도 초기화가 되지만 가급적 empty()사용
객체 값 가져오기
Optional 객체에 저장된 값을 가져올 땐 get( ) 메서드를 사용한다. 객체의 값이 null이라면 NoSuchElementException 에러가 발생하기 때문에 이때는 orElse( ) 메서드로 대체할 값을 정할 수 있다.
Optional<String> optVal = Optional.of("abcd"); //Optional 객체 생성
String str1 = optVal.get(); //optVal의 저장값 반환. null이면 에러
String str2 = optVal.orElse("default") //optVal에 null이 저장되어있으면 default를 반환
Optional (Java Platform SE 8 )
A container object which may or may not contain a non-null value. If a value is present, isPresent() will return true and get() will return the value. Additional methods that depend on the presence or absence of a contained value are provided, such as orEl
docs.oracle.com
'programming > JAVA' 카테고리의 다른 글
Java - 재귀 (0) | 2022.05.25 |
---|---|
Java - Effective cont. (0) | 2022.05.20 |
Java - Inner Class (0) | 2022.05.18 |
[TBC]Java - 컬렉션 프레임워크 (Collections Framework) (0) | 2022.05.17 |
Java - 지네릭 (generic) (0) | 2022.05.16 |