백엔드/코드스테이츠 수강

코드스테이츠 수강_5주차_2일차_JAVA_심화(애너테이션, 람다, 스트림, 파일 입출력)

반 불혹 2022. 9. 15. 23:09

코드스테이츠 수강 5주차 2일차에는 애너테이션, 람다, 스트림, 파일 입출력에 대해 배웟다. 

1. 애너테이션 

우리는 코딩 할 때 모든걸 손으로 치긴해도, 모든걸 기억할 순 없다.

그래서 주석으로 이 코드가 뭘 하는지 뭘 의미하는지 적어놓는데, 애너테이션도 비슷한 맥락이다.

애너테이션은 정보를 전달하고자 하는 대상이 사람이 아닌 다른프로그램(콤퓨타)인 것에 차이가 있다.

애너테이션의 대표적인 역할은

  • 컴파일러에게 문법체크를 하도록 정보제공 
  • 프로그램을 빌드 시 코드를 자동으로 생성하기 위한 정보 제공
  • 런타임(실행 중에) 특정 기능을 실행하도록 정보 제공 

예를들어, 애너테이션은 PC에게 "빌드할 때 이거 한번 문제있는지 봐라!" 라고 말해주는 것과 비슷하다. 

꼭 빌드가 아니여도 뭐 좀 유심히 봐달라 라고 말해주는 것과 같다. 

 

애너테이션의 종류

에너테이션에는 크게 3가지 종류가 있는데, 그 중에서 우리는 주로 2가지 (표준 애너테이션, 메타 애너테이션)을 사용한다. 

  • 표준 애너테이션: 자바에서 기본적으로 제공하는 애너테이션
@Override 컴파일러에게 메서드를 오버라이딩하는 것이라고 알림
@Deprecated 앞으로 사용하지 않을 대상을 알릴 때 사용
@FunctionalInterface 함수형 인터페이스라는 것을 알
@SuppressWarning 컴파일러가 경고메세지를 나타내지 않음
  • 메타 애너테이션: 애너테이션에 붙이는 애너테이션으로, 애너테이션을 정의하는 데에 사용
@Target 애너테이션을 정의할 때 적용 대상을 지정하는데 사용한다.
@Documented 애너테이션 정보를 javadoc으로 작성된 문서에 포함시킨다.
@Inherited 애너테이션이 하위 클래스에 상속되도록 한다.
@Retention 애너테이션이 유지되는 기간을 정하는데 사용한다.
@Repeatable 애너테이션을 반복해서 적용할 수 있게 한다.

위의 두개가 주로 사용도는 애너테이션이고, 나머지 하나는 사용자 애너테이션으로, 사용자가 직접 정의하여 사용한다. 

 

표준 애너테이션

표준 에너테이션은 자바가 기본적으로 제공하는 에너테이션이다.

대표적으로 많이 사용되는것 몇가지만 살펴보자 

@Override

@Override는 메서드 앞에만 붙일 수 있는 애너테이션으로 선언한 메서드가 상위 클래스의 메서드를 오버라이딩 하는 메서드라는 것을 컴파일러에게 알려주는 역할을 한다.

종종 오버라이딩 할 때 오타 등의 이유로 메서드의 이름을 잘못 작성하는 경우가 있다. 

class Super {
	void run() {}
}
class Sub extends Super {
	void rnu() {} // 오버라이딩을 하려고 했으나 오타가 발생함.
}

run()메서드를 rnu으로 오타 낸 상황.

이때는 서로 이름이 다르니 오버라이딩이 아니고, 그냥 새로운 메서드가 생겻나보다 하고 말아버린다. 

이를 방지하기 위해 @Override를 사용하는 것이다. 

class Super {
	void run() {}
}
class Sub extends Super {
	@Override
	void rnu() {} // 컴파일 에러 발생, 오타가 난 것을 발견할 수 있음.
}

위의 코드를 보면, @Override 가 밑의 rnu 메서드가 오타난 것을 감지하기 위해 넣은것을 볼 수 있다.

@Override해당 메서드(rnu으로 잘못 친 run)가 상위클래스(Super)의 메서드(run)을 오버라이딩 한 메서드 이며, 오버라이딩이 잘 된건지, 안된건지 검사해달라고 요청하는 애너태이션 인 것이다.
(이 상황에서는 오타 + 오버라이딩 한것이라고 알려준 것이니 컴파일러가 "님 실수함 ㅎ!" 하고 컴파일 에러를 알려준다. )

 

@Deprecated

@Deprecated는 모종의 이유로 더이상 사용되지 않는 필드, 메서드가 있는 경우에 사용한다.

이 애너테이션을 붙이면, "이거는 더이상 사용하지 않으니 주의해라" 라고 말해주는 것이다.

코드 예시

class OldClass {
	@Deprecated
	int oldField;

	@Deprecated
	int getOldField() { return oldField; };
}

위의 코드를 보면 oldField 변수와 getOldField()메서드가 이제는 사용되지 않는다고 말해주는 것이다. 

"왜 굳이 이렇게 남겨놓나? 그냥 삭제하면 되는데"

구버젼으로도 돌아갈 수 있게 하려고 이전에 되던것들을 남겨둘 때 사용될 수 있는 것이다.

 

@SuppressWarnings

@SuppressWarnings는 컴파일 경고 메시지가 나타나지 않도록 하는 애너테이션이다. 

영화에서 보면 우주선 경보가 나올 때 시끄럽다고 경보를 끈다. 딱 이 기능이다. 

@SuppressWarings(”all”) 모든 경고를 억제
@SuppressWarings(”deprecation”) Deprecated 메서드를 사용한 경우 나오는 경고 억제
@SuppressWarings(”fallthrough”) switch문에서 break 구문이 없을 때 경고 억제
@SuppressWarings(”finally”) finally 관련 경고 억제
@SuppressWarings(”null”) null 관련 경고 억제
@SuppressWarings(”unchecked”) 검증되지 않은 연산자 관련 경고 억제
@SuppressWarings(”unused”) 사용하지 않는 코드 관련 경고 억제

만약 위의 경고 억제 중에 몇개만 채택하고 싶으면 (한번에 여러개를 억제하고 싶으면)

@SuppressWarnings({"deprecation", "unused", "null"})

이르케 한번에 선언도 가능하다.

 

@FunctionalInterface

@FunctionalInterface은 함수형 인터페이스를 선언할 때 컴파일러가 함수형 인터페이스가 바르게 선언되었는지 확인하는 것이다. 

코드 예시 

@FunctionalInterface
public interface Runnable {
	public abstract void run (); // 하나의 추상 메서드
}

(함수형 인터페이스는 단 하나의 추상 메서드만 가져야 하는 제약이 있다.)

@FunctionalInterface없이도 함수형 인터페이스를 선언 가능 하지만, 실수방지용으로 넣어두는 것이다. 

 

메타 애너테이션

우리가 이전에 메타인지가 생각을 위한 생각 이라고 했듯이, 메타 애너테이션은 애너테이션을 위한 애너테이션이다.

대표적인것 일부만 보자.

@Target

@Target은 애너테이션을 적용할 대상을 지정할 때 사용한다.

코드 예시

import static java.lang.annotation.ElementType.*; 
//import문을 이용하여 ElementType.TYPE 대신 TYPE과 같이 간단히 작성할 수 있습니다.

@Target({FIELD, TYPE, TYPE_USE})	// 적용대상이 FIELD, TYPE
public @interface CustomAnnotation { }	// CustomAnnotation을 정의

@CustomAnnotation	// 적용대상이 TYPE인 경우
class Main {
    
		@CustomAnnotation	// 적용대상이 FIELD인 경우
    int i;
}

위의 코드에서 @Target뒤에 필드, 타입 등을 지정 한 것을볼 수 있다.

지정할 수 있는 타입은 아래와 같다.

ANNOTATION_TYPE 애너테이션
CONSTRUCTOR 생성자
FIELD 필드(멤버변수, 열거형 상수)
LOCAL_VARIABLE 지역변수
METHOD 메서드
PACKAGE 패키지
PARAMETER 매개변수
TYPE 타입(클래스, 인터페이스, 열거형)
TYPE_PARAMETER 타입 매개변수
TYPE_USE 타입이 사용되는 모든 대상

 

@Documented

@Documented은 애너테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 하는 설정이다. 

표준, 메타 애너테이션 중  @Override@SuppressWarnings를 제외하고 모두 @Documented가 적용 되어 있다. 

코드 예시 

@Documented
@Target(ElementType.Type)
public @interface CustomAnnotation { }

 

@Inherited

@Inherited는 하위 클래스가 애너테이션을 상속받도록 하는 애너테이션이다. 

해당 애너테이션을 상위클래스에 붙이면 하위 클래스도 상위 클래스에 붙은 애너테이션들이 동일하게 적용된다.

코드 예시

@Inherited // @SuperAnnotation이 하위 클래스까지 적용
@interface SuperAnnotation{ }

@SuperAnnotation
class Super { }

class Sub extends Super{ } // Sub에 애너테이션이 붙은 것으로 인식

위의 코드에서 Super를 상속받은 Sub에서도 @SuperAnnotation에 정의된 내용들을 적용 받는다.

 

사용자 정의 애너테이션

사용자가 직접 정의하는 애너테이션이다. 

인터페이스를 정의하는것과 비슷하다.

@interface 애너테이션명 { // 인터페이스 앞에 @기호만 붙이면 애너테이션을 정의할 수 있습니다. 
	타입 요소명(); // 애너테이션 요소를 선언
}

사용자 정의 애너테이션은 다른 클래스나 인터페이스를 상속 받을 수 없다.

 

2.람다

람다는 메서드를 하나의 식으로 표현한 것이다.

메서드를 일일히 선언해서 만들지 않아도 간단한 수학식처럼 간결하고 명확하게 표현이 가능하다. 

람다식 만들기

두 수를 더하는 메서드가 있다고 하자

int sum(int num1, int num2) {
	return num1 + num2;
}

이 메서드를 일단 람다식으로 만들면 

(int num1, int num2) -> { // 반환타입과 메서드명 제거 + 화살표 추가
	return num1 + num2;
}

위와 같이 메서드 명이 없어지고, 화살표(중요, 얘 있어야 댐)가 추가되고, 중괄호 안에 동작 내용(실행문)이 적힌다.

여기서 반환값이 있는 메서드는 return과 세미콜론(;)을 생략 가능하다.

(int num1, int num2) -> {
	num1 + num2
}

여기서 또 중괄호 없애고, 바로 반환값을 줄 수 있다. (실행문이 하나만 있기 때문)

(int num1, int num2) -> num1 + num2

여기서 또또 매개변수 타입이 쉽게 유추되는 경우면 (사실, 숫자 두개를 단순히 더하는거니까) 매개변수 타입을 생략 가능하다.

(num1, num2) -> num1 + num2

뭔가 엄청 축약된 람다식 완성

 

함수형 인터페이스

자바에서 함수는 클래스 안에서 정의 되어야 하기 때문에 메서드 독립으로 있을 수 없다.

그렇기 때문에 클래스 객체를 생성 후 이객체로 메서드를 호출해야 한다. 

그래서, 람다식도 사실 객체이다. 

위에서 메서드 이름을 지운것도 볼 수 있는데, 이름이 없는 상태이기 때문에 이를 익명클래스 라고 한다.

코드 예시

public class LamdaExample1 {
    public static void main(String[] args) {
		   /* Object obj = new Object() {
            int sum(int num1, int num2) {
                return num1 + num1;
            }
        };
			*/ 
		ExampleFunction exampleFunction = (num1, num2) -> num1 + num2
		System.out.println(exampleFunction.sum(10,15))
}

@FunctionalInterface // 컴파일러가 인터페이스가 바르게 정의되었는 지 확인할 수 있도록
interface ExampleFunction {
		public abstract int sum(int num1, int num2);
}

// 출력값
25

위의 코드에서 ExamplFunction 을 이용해서 객체를 만들고 그 안에 람다식을 넣엇다. 

그리고, 해당 객체에 sum메서드를 호출해서 10, 15를 넣은 것을 볼 수 있다. 
(기존의 인터페이스 문법을 활용하여 람다식을 다루는 것이다. )

이처럼 람다식과 인터페이스의 메서드가 1:1로 매칭되어 동작해야 한다.

 

매서드 래퍼런스

람다식으로 메서드를 호출 할 수 있다. 

만약, 두 개의 수 중 큰 수를 내놓는 메서드가 선언 되어 있다고 하면,

코드 예시

(left, right) -> Math.max(left, right);

위와 같이 작성이 가능하다.

이 경우, 입력값과 출력값의 반환타입을 쉽게 유추 할 수 있기 때문에 입력, 출력값을 적어주지 않아도 된다.

// 클래스이름::메서드이름

Math :: max; // 메서드 참조

고러면 이렇게 또 간단한 람다식이 생겨난다.

IntBinaryOperator 인터페이스는 두 개의 int 매개값을 받아 int 값을 리턴하므로, Math::max 메서드 참조를 대입할 수있다.

IntBinaryOperator operato = Math :: max; //메서드 참조

 

정적 메서드를 참조 할 때 

클래스 :: 메서드

인스턴스 메서드를 참조 할 때

참조 변수 :: 메서드

 

생성자 참조

메서드 참조는 생성자 참조도 포함된다. 

생성자 참조 = 객체생성

객체를 생성하고 반환하도록 구성된 람다식은 생성자 참조로 대체 가능하다.

코드 예시 

(a,b) -> {return new 클래스(a,b);};
//생성자 참조 문법

클래스 :: new

생성자가 오버로딩되서 어러개 있으면, 함수형 인터페이스의 추상 메서드와 동일한 매개 변수 타입과 개수를 가지고 있는 생성자를 찾아 실행한다.

 

3. 스트림

스트림은 배열, 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자이다.

스트림 특징 

선언형 프로그래밍이라고 하는데, 쉽게 말하면 "어떻게 하는지는 관심 없고, 뭘 하는지가 중요한 프로그래밍 패러다임"이다. 

선풍기에 전기가 얼만큼, 어느 시간동안 들어가는게 중요 한게 아니고, 전원 키면 바람 잘 나오는게 중요한거 처럼 말이다. 

스트림은 메서드는 함수형 인터페이스 매개타입을 가지기 때문에 람다식 또는 메서드 참조를 이용해서 요소 처리 내용을 매개값으로 전달할 수 있다.

스트림은 내부 반복자를 시용 가능하다.

컬렉션 내부에서 요소들을 반복시키기 때문에 그냥 요소당 처리할 코드만 제공하면 된다.

외부 반복자는 뭐냐고? 우리가 사용하던 for, while문이다.

외부반복자, 내부반복자 예시 (출처 : 코드스테이츠)

외부 반복자를 사용하면 컨렉션의 데이터를 가져오고, 그거 처리하고, 또 가져오고...를 반복하는데

내부 반복자를 사용하면 "그냥 이렇게해라" 라고 던져주면 컬렉션 내에서 알아서지지고 볶아서 결과값을 내준다.

일일히 확인하고 커리하지 말고 그냥 짬때리면 알잘딱깔센으로 내놓는다 이거다.

알잘딱깔센~!

이렇게 짬때리면 연산도 줄고, 내가 직접 뚜지 않아도 된다. 

이러한 특성으로 병렬작업이 가능하여(여러명한테 짬때리는거) CPU활용도가 높아진다.

중간연산과 최종연산을 할 수 있다. 

스트림을 진행하면서 나오는 중간의 결과들을 조회 가능하고, 당연히 최종값도 볼 수 있다. (뭐가 어떻게 흘러가는지 알 수 있다!)

 

파이프라인

스트림은 중간연산과 최종연산으로 동작한다. 

파이프라인은 여러개의 스트림이 연결되어 있는 구조를 의미한다. 
(필터링, 매핑, 정렬, 그루핑 등의 중간 연산스트림, 합계, 평균, 카운팅, 최대 / 최소값 등의 최종 연산 스트림이 연결되어 있다.)

파이프라인에서 최종 연산을 제외하고는 모두 중간 연산 스트림이다.

파이프라인의 모식도 (출처 : 코드스테이츠)

(시작)오리지널스트림 -> 필터링 -> 매핑 -> 최종 연산(끝) 으로 생각할 수 있는데, 사실 최종 연산이 시작되기 전까지는 중간연산이 지연된다.

(시작)(최종 (오리지널스트림 -> 필터링 -> 매핑) 연산 )(끝)의 형태를 띄는 것이다.

여기서 주의 할 것은 중간 연산은 스트림을 반환하고, 최종 연산은 결과값(int, String등의 데이터)를 내보낸다.

 

스트림 생성

Collection 인터페이스에는 stream()이 정의되어 있어 Collection 인터페이스를 구현한 객체들은 모두 stream()을 사용하여 스트림을 생성가능하다.
( stream()으로생성 시 스트림을 반환 )

코드 예시

// List로부터 스트림을 생성
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> listStream = list.stream();
listStream.forEach(System.out::prinln); //스트림의 모든 요소를 출력.

위의 코드에서 stream()으로 스트림을 반환 후, 최종연산(forEach)를 통해 스트림의 "요소"를 출력

배열의 원소를 소스로 하는 스트림Stream의 of 메서드 또는 Arrays의 stream 메서드를 사용한다.

코드 예시

// 배열로부터 스트림을 생성
Stream<String> stream = Stream.of("a", "b", "c"); //가변인자
Stream<String> stream = Stream.of(new String[] {"a", "b", "c"});
Stream<String> stream = Arrays.stream(new String[] {"a", "b", "c"});
Stream<String> stream = Arrays.stream(new String[] {"a", "b", "c"}, 0, 3); //end 범위 미포함

 

중간 연산, 최종연산 

앞서 스트림은 중간연산 , 최종연산으로 연결된다고 했다.

코드 예시 

intStream
	.filter(a -> a%2 ==0)
	.forEach(n -> System.out.println(n));

중간연산 filter와 최종연산 forEach를 통해 intStream의 원소를 출력아는 것을 볼 수 있다.

중산연산과 최종연산에 대한 메서드는 굉장히 많다.

모두를 다룰 순 없어서, 후에 번외로 올리거나, 일부만 다시 수정하여 올릴 듯 하다. 

 

Optional

Optional은 null 값으로 인해 에러가 발생하는 현상을 객체 차원에서 효율적으로 방지하고자 도입되었다.

Optional클래스는 모든 타입의 캑체를 담을 수 있는 래퍼클래스이다.

public final class Optional<T> {
	private final T value; // T타입의 참조변수
}

Optional 객체를 생성하려면 of() 또는 ofNullable()을 사용하고, 참조변수의 값이 null일 가능성이 있으면, ofNullable()을 사용한다.

코드 예시

Optional<String> opt1 = Optional.ofNullable(null);
Optional<String> opt2 = Optional.ofNullable("123");
System.out.println(opt1.isPresent()); //Optional 객체의 값이 null인지 여부를 리턴합니다.
System.out.println(opt2.isPresent());

Optional 타입의 참조변수를 기본값으로 초기화하려면 empty() 메서드를 사용한다.

Optional<String> opt3 = Optional.<String>empty();

Optional 객체에 저장된 값을 가져올 때는 get(), 값이 null일 가능성이 있으면 orElse()메서드로 초기갑슬 지정 가능하다.

Optional<String> optString = Optional.of("codestates");
System.out.println(optString);
System.out.println(optString.get());

String nullName = null;
String name = Optional.ofNullable(nullName).orElse("kimcoding"); 
//초기값을 kimcoding으로 지정하였다. null들어오면 이게 나옴
System.out.println(name);

Optional 객체는 스트림처럼 여러 메서드를 연결해서 작성 가능하다. 이를 메서드 체이닝이라고 한다.

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

public class OptionalExample {
    public static void main(String[] args) {
        List<String> languages = Arrays.asList(
                "Ruby", "Python", "Java", "Go", "Kotlin");
        Optional<List<String>> listOptional = Optional.of(languages);

        int size = listOptional
                .map(List::size)
                .orElse(0);
        System.out.println(size);
    }
}

 

4. 파일입출력

FileInputStream으로 파일 내용을 볼 수 있고, OutputStream으로 파일의 내용을 바꾸고, 파일로 만들 수 있다.

FileReader 으로 파일 내용을 볼 수 있고,  FileWriter 으로 파일의 내용을 바꾸고, 파일로 만들 수 있다.

위와 아래의 차이는 입출력 단위다. FileInputStream, OutputStream은 1바이트 단위로 입출력되고, FileReader ,FileWriter  는 2바이트 단위로 입출력된다.

자바에서는 char가 2바이트 이므로, 1바이트만 사용하는FileInputStream, OutputStream을 해결 하기 위해서 FileReader ,FileWriter 문자 기반 스트림을 사용한다.

  • 바이트기반 : FileInputStream, OutputStream

  • 문자기반 :FileReader ,FileWriter

 

 

오늘도 힘들엇다.

뒤로갈수록 지치고, 손목이 이제는 많이 삐그덕 거린다. 

아마 키보드, 마우스에 손이 많이 부하가 오는거 같다. 

좀 자세히 쓰려다가 욕심인거 같다... 요약해서 쓰도록 하면서 요약을 하기위해 공부도 하면서 써야겟다.