목차
- enum
- 애너테이션
- 람다
- 스트림
- 파일 입출력
배운 내용
Enum
기능
- 여러 상수들을 보다 편리하게 선언하고 관리할 수 있게한다.
- 상수명의 중복을 피하고, 타입에 대한 안정성을 보장
- 더 간결하고 가독성이 좋은 코드를 작성
- switch문에서도 작동이 가능
문법
enum 열거형이름 { 상수명1, 상수명2, 상수명3, ...}
- 관례적으로 대문자로 작성
- 각각의 열거 상수들은 객체이다.
- 자동적으로 0부터 시작하는 정수값이 할당되어 각각의 상수를 가리킨다.
- 상수에 접근하는 방법 :
열거형이름.상수명
메서드
리턴 타입 | 메소드(매개변수) | 설명 |
---|
String | name() | 열거 객체가 가지고 있는 문자열을 리턴하며, 리턴되는 문자열은 열거타입을 정의할 때 사용한 상수 이름과 동일합니다. |
int | ordinal() | 열거 객체의 순번(0부터 시작)을 리턴합니다. |
int | compareTo(비교값) | 주어진 매개값과 비교해서 순번 차이를 리턴합니다. |
열거 타입 | valueOf(String name) | 주어진 문자열의 열거 객체를 리턴합니다. |
열거 배열 | values() | 모든 열거 객체들을 배열로 리턴합니다. |
활용 예시
public static final int SPRING = 1;
public static final int SUMMER = 2;
public static final int FALL = 3;
public static final int WINTER = 4;
public static final int DJANGO = 1;
public static final int SPRING = 2;
public static final int NEST = 3;
public static final int EXPRESS = 4;
-------------------------------------
interface Seasons {
int SPRING = 1, SUMMER = 2, FALL = 3, WINTER = 4;
}
interface Frameworks {
int DJANGO = 1, SPRING = 2, NEST = 3, EXPRESS = 4;
}
if (Seasons.SPRING == Frameworks.SPRING)
class Seasons {
public static final Seasons SPRING = new Seasons();
public static final Seasons SUMMER = new Seasons();
public static final Seasons FALL = new Seasons();
public static final Seasons WINTER = new Seasons();
}
class Frameworks {
public static final Frameworks DJANGO = new Frameworks();
public static final Frameworks SPRING = new Frameworks();
public static final Frameworks NEST = new Frameworks();
public static final Frameworks EXPRESS = new Frameworks();
}
----------------------------------------------------------------
enum Seasons { SPRING, SUMMER, FALL, WINTER }
enum Frameworks { DJANGO, SPRING, NEST, EXPRESS }
enum Seasons { SPRING, SUMMER, FALL, WINTER }
public class EnumExample {
public static void main(String[] args) {
Seasons favoriteSeason = Seasons.SPRING;
System.out.println(favoriteSeason);
}
}
enum Seasons {SPRING, SUMMER, FALL, WINTER}
public class Main {
public static void main(String[] args) {
Seasons seasons = Seasons.SPRING;
switch (seasons) {
case SPRING:
System.out.println("봄");
break;
case SUMMER:
System.out.println("여름");
break;
case FALL:
System.out.println("가을");
break;
case WINTER:
System.out.println("겨울");
break;
}
}
}
봄
애너테이션
기능
- 주석은 소스 코드를 읽는 ‘사람’에게 정보를 제공하는 반면, 애너테이션은 특정 코드를 사용하는 ‘프로그램’에게 정보를 전달
- 정보 전달과 다른 프로그램에게 유용한 정보를 제공
- 타겟 외의 다른 프로그램들에게는 아무런 영향을 주지 않는다.
역할
- 주로, 자바 컴파일러에게 어떤 정보를 제공하기 위한 역할
- 컴파일러에게 문법 에러를 체크하도록 정보를 제공합니다.
- 프로그램을 빌드할 때 코드를 자동으로 생성할 수 있도록 정보를 제공합니다.
- 런타임에 특정 기능을 실행하도록 정보를 제공합니다.
종류
표준 애너테이션
표준 애너테이션 | 설명 |
---|
@Override | 컴파일러에게 메서드를 오버라이딩하는 것이라고 알림 |
@Deprecated | 앞으로 사용하지 않을 대상을 알릴 때 사용 |
@FunctionalInterface | 함수형 인터페이스라는 것을 알림 |
@SuppressWarning | 컴파일러가 경고메세지를 나타내지 않음 |
메타 애너테이션
- 애너테이션에 붙이는 애너테이션으로, 애너테이션을 정의하는 데에 사용
메타 애너테이션 | 설명 |
---|
@Target | 애너테이션을 정의할 때 적용 대상을 지정하는데 사용한다. |
@Documented | 애너테이션 정보를 javadoc으로 작성된 문서에 포함시킨다. |
@Inherited | 애너테이션이 하위 클래스에 상속되도록 한다. |
@Retention | 애너테이션이 유지되는 기간을 정하는데 사용한다. |
@Repeatable | 애너테이션을 반복해서 적용할 수 있게 한다. |
import static java.lang.annotation.ElementType.*;
@Target({FIELD, TYPE, TYPE_USE})
@Retention - 애넡테이션 지속싯간
유지 정책 | 설명 |
---|
SOURCE | 소스 파일에 존재, 클래스파일에는 존재하지 않음 |
CLASS | 클래스 파일에 존재, 실행시에 사용불가, 기본값 |
RUNTIME | 클래스 파일에 존재, 실행시에 사용가능 |
**@Repeatable**
- 같은 이름의 애너테이션이 여러번 적용될 수 있기 때문에, 이 애너테이션들을 하나로 묶어주는 애너테이션도 별도로 작성해야한다.
@interface Works {
Work[] value();
}
@Repeatable(Works.class)
@interface Work {
String value();
}
사용자 정의 애너테이션
@interface 애너테이션명 {
타입 요소명();
}
람다(Lambda)
- 함수형 프로그래밍 기법을 지원하는 자바의 문법요소
- 메서드를 하나의 ‘식(expression)’으로 표현한 것으로, 코드를 매우 간결하면서 명확하게 표현할 수 있다
- 공통기능을 일반적인 함수처럼 독립적으로 만든 후 모든 클래스에서 공통적으로 사용할 수있게 하기위해
- 기본적으로 반환타입과 이름을 생략가능
- 람다식 또한 사실은 객체이다 → 익명 이너 클래스의 축약된 형태
- 익명 클래스란 객체의 선언과 생성을 동시에 하여 오직 하나의 객체를 생성하고, 단 한번만 사용되는 일회용 클래스입니다.
특정조건 충족시
- return문과 문장 뒤에 오는 세미콜론(;) 생략가능
- 실행문이 하나만 존재할 때 중괄호를 생략가능
- 매개변수 타입을 쉽게 유추할 수 있는 경우에는 매개변수의 타입을 생략가능
int sum(int num1, int num2) {
return num1 + num2;
}
(int num1, int num2) -> {
num1 + num2
}
(int num1, int num2) -> num1 + num2
(num1, num2) -> num1 + num2
------------------------------------
(num1, num2) -> num1 + num2
new Object() {
int sum(int num1, int num2) {
return num1 + num1;
}
}
함수형 인터페이스(Functional Interface)
- 기존의 인터페이스 문법을 활용하여 람다식을 다루는 것
- 람다식도 결국 하나의 객체이기 때문에 인터페이스에 정의된 추상메서드를 구현
할 수 있기 때문
- 단 하나의 추상메서드만 선언될 수있다
- 람다식과 인터페이스의 메서드가 1:1로 매칭되어야 하기 때문
메서드 레퍼런스
- 불필요한 매개변수를 제거할때 주로 사용
- 람다식으로 간단해진 익명객체를 더욱더 간단하게 사용하고 싶을때 사용
- 입력값과 출력값의 반환타입을 쉽게 유추할 수있을때, 단순히 메서드 호출로 구성된 람다식을 메서드 참조로 대치
**정적 메서드와 인스턴스 메서드 참조**
- 정적 메서드 :
클래스 :: 메서드
A a = B::bcd;
- 인터페이스 A의 객체를 생성할 때 구현해야 하는 abc() 메서드를 B.bcd()와 동일하게 하라.
- 인스턴스 메서드 :
참조 변수 :: 메서드
B b = new B();
A a = b::bcd;
- A 인터페이스 내부의 abc()메서드는 참조변수 b 객체 내부의 인스턴스 메서드 bcd()와 동일하다는 의미
public class MethodReferences {
public static void main(String[] args) throws Exception {
IntBinaryOperator operator;
operator = Calculator::staticMethod;
System.out.println("정적메서드 결과 : " + operator.applyAsInt(3, 5));
Calculator calculator = new Calculator();
operator = calculator::instanceMethod;
System.out.println("인스턴스 메서드 결과 : "+ operator.applyAsInt(3, 5));
}
}
**생성자 참조**
- 생성자를 참조한다는 것은 객체 생성을 의미
- 단순히 메서드 호출로 구성된 람다식을 메서드 참조로 대치할 수 있듯이, 단순히 객체를 생성하고 리턴하도록 구성된 람다식은 생성자 참조로 대치 가능
클래스 :: new
- 생성자가 오버로딩 되어 여러 개가 있을 경우 컴파일러는 함수형 인터페이스의 추상 메서드와 동일한 매개 변수 타입과 개수를 가지고 있는 생성자를 찾아 실행
스트림(Stream)
정의
- 배열, 컬렉션의 저장 요소를 하나씩 참조해서 람다식으로 처리할 수 있도록 해주는 반복자
- 데이터를 연속적으로 전달하는 통로
특징
- 선언형으로 데이터 소스를 처리
- 선언형 프로그래밍 : “어떻게" 수행하는지보다는 “무엇을” 수행하는 지에 중점을두는 프로그래밍
- 내부 동작 원리를 모르더라도 코드가 무슨 일을 하는지 이해가능
- Stream이 제공하는 대부분의 요소 처리 메서드는 함수형 인터페이스 매개타입을 가지기 때문에 람다식 또는 메서드 참조를 이용해서 요소 처리 내용을 매개값으로 전달할 수 있습니다.
- **내부 반복자를 사용하므로 병렬 처리가 쉽다.**
- 외부반복자(external iterator)
- 개발자가 코드로 직접 컬렉션의 요소를 반복해서 가져오는 코드 패턴
- for문, Iterator를 이용한 while문
- 내부 반복자(internal iterator)
- 컬렉션 내부에서 요소들을 반복시키고 개발자는 요소당 처리해야할 코드만 제공하는 코드 패턴
- Iterator는 컬렉션의 요소를 가져오는 것에서부터 처리하는 것까지 모두 개발자가 작성해야 하지만 스트림은 람다식으로 요소 처리 내용만 전달할 뿐, 반복은 컬렉션 내부에서 일어납니다.
활용 예시
import java.util.List;
public class DeclarativeProgramingExample {
public static void main(String[] args){
List<Integer> numbers = List.of(1, 3, 6, 7, 8, 11);
int sum =
numbers.stream()
.filter(number -> number > 4 && (number % 2 == 0))
.mapToInt(number -> number)
.sum();
System.out.println("# 선언형 프로그래밍: " + sum);
}
}
public class StreamLambdaExample {
public static void main(String[] args) throws Exception {
List<Student> list = Arrays.asList(
new Student("김코딩", 95),
new Student("이자바", 92)
);
Stream<Student> stream = list.stream();
stream.forEach( s -> {
String name = s.getName();
int score = s.getScore();
System.out.println(name+ " - " +score);
});
}
}
파이프라인 구성
리덕션(Reduction)
- 대량의 데이터를 가공해서 축소하는 것
- 데이터의 합계, 평균값, 카운팅, 최대, 최소값 등
파이프라인
- 여러개의 스트림이 연결되어 있는 구조
- 파이프라인에서 최종 연산을 제외하고는 모두 중간 연산 스트림
Stream<Member> maleFemaleStream = list.stream();
Stream<Member> maleStream = maleFemaleSTream.filter(m -> m.getGender() == Member.MALE);
IntStream ageStream = maleStream.mapToInt(Member::getAge);
OptionalDouble opd = ageStream.average();
double ageAve = opd.getAsDouble();
.filter(m-> m.getGender() == Member.MALE)
는 남자 Member 객체를 요소로 하는 새로운 스트림을 생성한다.
.mapToInt(Member::getAge)
는 Member 객체를 age 값으로 매핑해서 age를 요소로 하는 새로운 스트림을 생성한다.
average()
메소드는 age 요소의 평균을 OptionalDouble에 저장합니다. OptionalDouble에 저장된 평균 값을 읽으려면 getAsDouble()
메소드를 호출 하면 된다.
**스트림 생성, 중간 연산, 최종 연산**
**스트림 생성**
- Collection 인터페이스에는
stream()
이 정의되어 있기 때문에, 컬렉션은 모두 이 메서드를 이용해 스트림을 생성
stream()
을 사용하면 해당 Collection의 객체를 소스로 하는 Stream을 반환
- 스트림은 데이터 소스로부터 데이터를 읽기만 할 뿐 변경하지 않는다.(Read-only).
- 스트림은 일회용, 필요하다면 새로운 스트림을 다시 만들어야 한다.
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> listStream = list.stream();
listStream.forEach(System.out::prinln);
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);
**중간 연산**
- 연산 결과를 스트림으로 반환하기 때문에, 연속해서 여러 번 수행가능
**필터링(filter(), distinct())**
distinct()
: Stream의 요소들중 중복데이터를 제거하기 위해 사용
filter()
: Stream에서 조건에 맞는 데이터만을 정제하여 더 작은 컬렉션을 생성. 매개값으로 조건(Predicate)이 주어지고, 조건이 참이 되는 요소만 필터링한다.
매핑(map())
map()
: 기존의 Stream 요소들을 대체하는 요소로 구성된 새로운 Stream을 형성하는 연산
- 일반적인 Stream 객체를 원시 Stream으로 바꾸거나 그 반대로 하는 작업이 필요한 경우에 사용
flatMap()
: 요소를 대체하는 복수 개의 요소들로 구성된 새로운 스트림을 리턴
**정렬(sorted())**
- 정렬하기 위해 사용, 파라미터로 Comparator 사용가능
- 인자없이 호출시 오름차순, 내림차순은 reverseOrder사용
**연산 결과 확인(peek())**
- 요소를 하나씩 돌면서 출력
- 연산 중간에 결과를 확인하여 디버깅하고자 할 때 사용
최종 연산
연산 결과 확인**(forEach())**
**매칭(match())**
- Stream의 요소들이 특정한 조건을 충족하는지 검사
- 함수형 인터페이스 Predicate를 받아서 해당 조건을 만족하는지 검사하고, 검사 결과를 boolean으로 반환
allMatch()
: 모든 요소들이 조건을 만족하는지 조사
anyMatch()
: 최소한 한 개의 요소가 조건을 만족하는지 조사
noneMatch()
: 모든 요소들이 조건을 만족하지 않는지 조사
기본집계**(sum(), count(), average(), max(), min())**
import java.util.Arrays;
public class AggregateExample {
public static void main(String[] args) throws Exception {
int[] intArr = {1,2,3,4,5};
long count = Arrays.stream(intArr).count();
System.out.println("intArr의 전체 요소 개수 " + count);
long sum = Arrays.stream(intArr).sum();
System.out.println("intArr의 전체 요소 합 " + sum);
double avg = Arrays.stream(intArr).average().getAsDouble();
System.out.println("전체 요소의 평균값 " + avg);
int max = Arrays.stream(intArr).max().getAsInt();
System.out.println("최대값 " + max);
int min = Arrays.stream(intArr).min().getAsInt();
System.out.println("최소값 " + min);
int first = Arrays.stream(intArr).findFirst().getAsInt();
System.out.println("배열의 첫번째 요소 " + first);
}
}
**reduce()**
- 누적하여 하나로 응축(reduce)하는 방식
- 두 요소의 연산 결과를 바탕으로 다음 요소와 연산
- 최대 3개의 매개변수를 받을 수 있다.
Accumulator
: 각 요소를 계산한 중간 결과를 생성하기 위해 사용
Identity
: 계산을 수행하기 위한 초기값
Combiner
: 병렬 스트림(Parlallel Stream)에서 나누어 계산된 결과를 하나로 합치기 위한 로직
**collect()**
- Stream의 요소들을 List나 Set, Map, 등 다른 종류의 결과로 수집하고 싶은 경우 사용
List<Student> maleList = totalList.stream()
.filter(s -> s.getGender() == Student.Gender.Male)
.collect(Collectors.toList());
Set<Student> femaleSet = totalList.stream()
.filter(s -> s.getGender() == Student.Gender.Female)
.collect(Collectors.toCollection(HashSet :: new));
Optional
- null 값으로 인해 에러가 발생하는 현상을 객체 차원에서 효율적으로 방지하고자 도입
Optional
- 모든 타입의 객체를 담을 수 있는 래퍼(Wrapper) 클래스
- Stream 처럼 사용하기 :Optional을 제대로 사용하려면, Optional을 최대 1개의 원소를 가지고 있는특별한 Stream이라고 생각하면좋다.
public final class Optional<T> {
private final T value;
}
- Optional 객체를 생성하려면
of()
또는 ofNullable()
을 사용
- 참조변수의 값이 null일 가능성이 있다면,
ofNullable()
을 사용
- 스트림과 유사하게 여러 메서드를 연결해서 작성할 수 있다.(메서드 체이닝)
Optional<String> opt1 = Optional.ofNullable(null);
Optional<String> opt2 = Optional.ofNullable("123");
System.out.println(opt1.isPresent());
System.out.println(opt2.isPresent());
Optional<String> opt3 = Optional.<String>empty();
Optional<String> optString = Optional.of("codestates");
System.out.println(optString);
System.out.println(optString.get());
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");
System.out.println(name);
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);
}
}
파일 입출력(I/O)
- 스트림은 단방향으로만 데이터를 전송할 수 있기에, 입력과 출력을 동시에 처리하기 위해서는 각각의 스트림이 필요
public class FileInputStreamExample {
public static void main(String args[])
{
try {
FileInputStream fileInput = new FileInputStream("codestates.txt");
BufferedInputStream bufferedInput = new BufferedInputStream(fileInput);
int i = 0;
while ((i = bufferedInput.read()) != -1) {
System.out.print((char)i);
}
fileInput.close();
}
catch (Exception e) {
System.out.println(e);
}
}
}
**FileOutputStream**
public class FileOutputStreamExample {
public static void main(String args[]) {
try {
FileOutputStream fileOutput = new FileOutputStream("codestates.txt");
String word = "code";
byte b[] = word.getBytes();
fileOutput.write(b);
fileOutput.close();
}
catch (Exception e) {
System.out.println(e);
}
}
}
FileReader
- 문자 기반 스트림
- FileReader는 인코딩을 유니코드로 변환하고, FileWriter는 유니코드를 인코딩으로 변환
public class FileReaderExample {
public static void main(String args[]) {
try {
String fileName = "codestates.txt";
FileReader file = new FileReader(fileName);
int data = 0;
while((data=file.read()) != -1) {
System.out.print((char)data);
}
file.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
FileWriter
public class FileWriterExample {
public static void main(String args[]) {
try {
String fileName = "codestates.txt";
FileWriter writer = new FileWriter(fileName);
String str = "written!";
writer.write(str);
writer.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
File
- File 클래스로 파일과 디렉토리에 접근
- 일을 생성하기 위해서는 파일 인스턴스를 생성할 때 다음과 같이 첫 번째 인자에 경로를, 두 번째 인자에 파일명을 작성하고, createNewFile()메서드를 호출해주어야 한다.
File file = new File("./", "newCodestates.txt");
file.createNewFile();
public class FileClassExample {
public static void main(String[] args) {
File parentDir = new File("./");
File[] list = parentDir.listFiles();
String prefix = "code";
for(int i =0; i <list.length; i++) {
String fileName = list[i].getName();
if(fileName.endsWith("txt") && !fileName.startsWith("code")) {
list[i].renameTo(new File(parentDir, prefix + fileName));
}
}
}
}
어려운 내용(에러)
문제
해결