3 레벨에서는 Enum, 람다, 제네릭 클래스, 스트림을
이용해 보는 것이기 때문에 어제 맛만 봤던 Enum과 람다는 제쳐두고
제네릭과 스트림의 대해 알아보고 이용해 봤다.
제네릭이란
클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미합니다.
제네릭의 장점
- 제네릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.
- 클래스 외부에서 타입을 지정해 주기 때문에 따로 타입을 체크하고 변환해 줄 필요가 없다. 즉, 관리하기가 편하다.
- 비슷한 기능을 지원하는 경우 코드의 재사용성이 높아진다.
제네릭 사용방법
제네릭은 아래표의 타입들이 많이 쓰인다.
타입 | 설명 |
<T> | Type |
<E> | Element |
<K> | Key |
<V> | Value |
<N> | Number |
물론 반드시 한 글자일 필요도 없고 위에 표와 반드시 일치해야 할 필요는 없지만
대중적으로 통하는 통상적인 선언이 가장 편하기 때문에
위와 같은 암묵적인 규칙이 있을 뿐이다.
클래스 및 인터페이스 선언
public class ArithmeticCalculator <T> {...}
public interface Calculator <T> {...}
기본적으로 제네릭 타입의 클래스나 인터페이스의 경우 위와 같이 선언한다.
T 타입은 해당 블록 {...} 안에서까지 유효한다.
또한 제네릭 타입을 두 개로 둘 수 있다. 대표적으로 타입 인자로 두 개를 받는 Map이 있다.
public class Map<K, V>{}
public class Main{
public static void main(String [] args){
Map<String, Integer> a = new Map<String,Integer>();
}
}
위 예시대로라면 K는 String이 되고, V는 Integer가 된다.
이때 주의해야할 점은 타입 파라미터로 명시할 수 있는 것은 참조타입밖에 올 수 없다. 즉, int, double, char 같은 원시 타입형은 올 수 없다는 것이다.
참조 타입뿐만 아니라 사용자가 정의한 클래스 타입도 타입으로 올 수 있다.
public class T<T> {...}
public Class Example {...}
public class Main{
public static void main(String [] args){
T<Example> a = new T<Example>();
}
}
이와 같은 방법으로 제네릭 클래스를 만들어 봤다.
import java.util.ArrayList;
import java.util.List;
// T extends Number로 해두면 클래스 또는 메소드 선언에서 제네릭 타입 T가 Number 또는
// 그 하위 클래스를 의미하도록 제한한다.
public class ArithmeticCalculator<T extends Number> {
private final ArrayList<T> arrayList = new ArrayList<>();
private T firstNumber;
private T secondNumber;
public void add(T results) {
this.arrayList.add(results);
}
public void setFirstNumber(T firstNumber) {
this.firstNumber = firstNumber;
}
public void setSecondNumber(T secondNumber) {
this.secondNumber = secondNumber;
}
public T getFirstNumber() {
return firstNumber;
}
public T getSecondNumber() {
return secondNumber;
}
// 리스트가 비어있는지 확인
public boolean arrayIsEmpty() {
return arrayList.isEmpty();
}
// 0번째 방 데이터 삭제
public void indexZeroRemove() {
arrayList.remove(0);
}
// 원하는 방의 데이터 삭제
public void wantIndexRemove(int idxNumber){
arrayList.remove(idxNumber);
}
// 받아온 인덱스 번호의 값 전달
public T getArrNumber(int idxNumber) {
return arrayList.get(idxNumber);
}
// 받아온 index 방번호에 바꿀 값 저장.
public void setArrValue(int idxNumber, T changeValue) {
arrayList.set(idxNumber, changeValue);
}
// 리스트의 저장된 모든 값을 List 형태로 전달
public List<T> getArrayList() {
return arrayList.stream().toList();
}
}
CalculatorApp 클래스에서는 정규식을 이용하여
예외처리를 해준 뒤 각각의 메서드를 실행해 준다.
import java.util.List;
import java.util.regex.Pattern;
public class CalculatorApp {
private final ArithmeticCalculator<Double> doubleCalculator
= new ArithmeticCalculator<>();
private OperatorType operator;
// 정수, 실수, 연산 기호 유효성 검사
enum Reg {
OPERATION_REG("[+\\-*/]"),
NUMBER_REG("^[0-9]*$"),
DOUBLE_LEG("^([0-9]{1}\\d{0,2}|0{1})(\\.{1}\\d{0,1})*$");
private final String reg;
Reg(String reg) {
this.reg = reg;
}
}
// 입력받아온 숫자가 숫자형식인지 정규식으로 비교한 후 숫자가 맞다면
// double 형태로 형변환을 하는 동시에 firstNumber에 값을 저장.
public void setFirstNumber(String firstNumber) throws Exception {
if (!Pattern.matches(Reg.NUMBER_REG.reg, firstNumber) ||
!Pattern.matches(Reg.DOUBLE_LEG.reg, firstNumber)) {
throw new BadInputException("정수, 실수");
}
doubleCalculator.setFirstNumber(Double.parseDouble(firstNumber));
}
// 입력받아온 숫자가 숫자형식인지 정규식으로 비교한 후 숫자가 맞다면
// double 형태로 형변환을 하는 동시에 secondNumber에 값을 저장.
public void setSecondNumber(String secondNumber) throws Exception {
if (!Pattern.matches(Reg.DOUBLE_LEG.reg, secondNumber) ||
!Pattern.matches(Reg.DOUBLE_LEG.reg, secondNumber)) {
throw new BadInputException("정수, 실수");
}
doubleCalculator.setSecondNumber(Double.parseDouble(secondNumber));
}
// 리스트의 저장된 모든 값을 가져와서 스트림을이용해 sout로 값들 출력
public void getArrayList() {
List<Double> list = doubleCalculator.getArrayList();
// stream forEach문을 이용하여 sout를 출력하려 했지만
// 람다 표현식에 사용되는 변수는 final 또는 유사 final이어야 하기 때문에
// 리스트의 값들 앞에 1. 2. 이런식으로 붙여줄 수가 없다.
//list.stream().toList().forEach(li -> System.out.println(li.doubleValue()));
int num = 1;
// 그래서 향상된 for문을 이용하여 출력해준다.
for (Double li : list) {
System.out.println(num + ". " + li);
num++;
}
}
// 입력 받은 연산 기호가 연산기호 이외에 다른 문자가 아닌지 판별 후
// 우리가 정해둔 연산기호가 맞다면 OperatorType 클래스에 만들어둔 getOp 메서드 실행.
// 아니라면 예외 처리
public void setUpOperation(String operation) throws Exception {
if (!Pattern.matches(Reg.OPERATION_REG.reg, operation)) {
throw new BadInputException("연산 기호");
}
this.operator = OperatorType.getOperator(operation);
}
// 받아온 인덱스 번호의 값 전달해주는 메서드 실행.
public double getArrNumber(int idxNumber) {
return doubleCalculator.getArrNumber(idxNumber);
}
// 연산과 연산 결과 리스트의 저장하는 메서드 실행
public double calculator() {
double x = doubleCalculator.getFirstNumber();
double z = doubleCalculator.getSecondNumber();
double result = operator.calculate(x, z);
doubleCalculator.add(result);
return result;
}
// 입력 받은 값보다 큰 값들 리스트 받아서 빠른for문으로 sout 출력
public void getLargeNumber(double number) {
List<Double> list = doubleCalculator.getArrayList();
List<Double> lageNumbers = operator.getLargeNumber(list, number);
int num = 1;
for (double li : lageNumbers) {
System.out.println(num + ". " + li);
num++;
}
}
// 받아온 입력값이 정수or실수가 맞는지 확인 후 맞다면 리스트 값을 수정해주는
// set 메서드 실행 아니라면 예외처리
public void setArrNumber(String targetNumber, String changeValue) throws Exception {
if (!Pattern.matches(Reg.NUMBER_REG.reg, targetNumber) ||
!Pattern.matches(Reg.DOUBLE_LEG.reg, changeValue)) {
throw new BadInputException("정수");
}
// 존재하는 리스트의 배열 길이를 초과하는 번호 입력 시 예외 처리를 해준다.
else if(Integer.parseInt(targetNumber) > doubleCalculator.getArrayList().size()) {
throw new IndexOutOfBoundsException("번호를 잘 확인해주세요");
}
int idxNumber = Integer.parseInt(targetNumber) - 1;
doubleCalculator.setArrValue(idxNumber, Double.parseDouble(changeValue));
}
// 가장 맨 앞에 저장되어 있는 데이터 삭제하는 메서드 실행
public void indexZeroRemove() {
// 컬렉션에 저장된 데이터가 하나도 없을때는 더 이상 삭제할 데이터가 없다는 문구 출력
if (doubleCalculator.arrayIsEmpty()) {
System.out.println("더 이상 삭제할 데이터가 없습니다.");
}
// 데이터가 하나 이상있다면 제일 먼저 저장된 0번방의 데이터를 삭제 후
// 삭제 완료 문구 출력
else {
doubleCalculator.indexZeroRemove();
System.out.println("삭제 완료 !");
}
}
// 가장 높은 값 전달
public double getMaxNumber(){
// doubleCalculator.getArrayList 메서드는 리스트 형태로 값을 전달하기 때문에
// getMaxNumber 매개변수로 바로 전달한다.
return operator.getMaxNumber(doubleCalculator.getArrayList());
}
// 가장 작은 값 전달
public double getMinNumber(){
// doubleCalculator.getArrayList 메서드는 리스트 형태로 값을 전달하기 때문에
// getMaxNumber 매개변수로 바로 전달한다.
return operator.getMinNumber(doubleCalculator.getArrayList());
}
// 원하는 방의 데이터 삭제
public void wantIndexRemove(int idxNumber){
doubleCalculator.wantIndexRemove(idxNumber);
}
}
스트림 활용해보기전에 스트림 알아보기
스트림이란
Java 8 부터 추가된 기술로 람다를 활용해 배열과 컬렉션을 함수형으로 간단하게 처리할 수 있는 기술이다.
스트림은 데이터 소스를 추상화하고, 데이터를 다루는데 자주 사용되는 메서드를 정의해 놓아서 데이터 소스에 상관없이 모두 같은 방식으로 다룰 수 있으므로 코드의 재사용성이 높아진다.
스트림의 특징
- 원본 데이터 소스를 변경하지 않는다.
- 일회용이다
- 최종 연산 전까지 중간 연산을 수행하지 않는다.
- 작업을 내부 반복으로 처리한다 : forEach()는 매개변수에 대입된 람다식을 데이터 소스의 모든 요소에 적용한다.
- 기본형 스트림을 제공한다 - Stream<Integer> 대신 IntStream이 제공되어서 오토박싱과 언박싱 등의 불필요한 과정이 생략되고 숫자의 경우 유용한 메서드를 추가로 제공한다.
스트림 만들기
배열 스트림 : Arrays.stream()
String [] arr = new String[]{"adasd","basd"};
Stream<String> stream = Arrays.stream(arr);
컬렉션 스트림 : .stream()
List <String> list = Arrays.asList("a","BB","c");
Stream <String> stream = list.stream();
Stream.builder()
Stream <String> builderStream = Stream.<String> builder()
.add("a").add("b").add("c")
.build();
람다식 Stream.generate(), iterate()
Stream <String> genratedStream = Stream.generate(() -> "a").limit(3);
// 생성할 때 스트림의 크기가 정해져 있지 않기(무한하기)때문에 최대 크기를 제한해줘야 한다.
Stream <Integer> iteratedStream = Stream.iterate(0, n -> n+2).limit(5); // 0,2,4,6,8
기본 타입형 스트림
IntStream intStream = IntStream.range(1,5); // [1,2,3,4]
IntStream intStream2 = IntStream.rangeClosed(1,5); // [1,2,3,4,5]
// range()는 두 번째 파라미터를 범위에 포함하지 않지만
// rangeClosed()는 두 번째 파라미터도 범위에 포함시킨다.
filter 메서드
Stream 클래스에서 제공하는 메서드로 스트림 요소를 순회하면서 특정 조건을 만족하는 요소로 구성된 새로운 스트림을 반환합니다.
filter 메서드는 람다 표현식도 전달할 수 있습니다.
.toList()
toList()는 스트림 인터페이스에 존재하는 default 메소드이다.
스트림 인터페이스에 존재하는 toList()로 반환된 리스트는 수정할 수 없으며,
어떠한 변경 메소드를 호출 하더라도 항상 UnsupportedOperationException이 발생한다.
collect()
collect()는 Java 스트림 API에서 제공하는 메소드이다. 이 메서드는 스트림의 마지막에 위치하는 최종 연산이며, 스트림의 요소를 처리하고 그 결과를 반환합니다.
Collector 인터페이스에 정의된 다양한 메소드를 통해 원하는 기능 및 작업 흐름에 맞게 스트림 요소들을 그룹화 하거나 집계할 수 있다.
Collectors.toList()
Collector는 Java의 Stream API에서 제공하는 인터페이스로 스트림의 요소들을 어떤 방식으로 수집할지 정의한다.주로 collect()와 함께 쓰인다.
List <Double> number = list.stream()
.filter(num -> num >5)
.collect(Collectors.toList());
Collectors.toList() : 스트림의 요소들을 리스트로 수집collect() : 원하는 결과 형태로 수집 후 결과 반환또한 위에서 봤던 toList()는 반환된 리스트는 수정이 불가능했지만Colletcors.toList()는 반환된 리스트 수정이 가능하다.
스트림에서 제공하는 메서드나 인터페이스는 위에서 정리한 것 말고도 많지만
위의 내용을 바탕으로 스트림을 이용해봤다.
public static OperatorType getOperator(String operator){
return Arrays.stream(OperatorType.values()).filter(oper -> oper.symbol.equals(operator)).findFirst().
orElseThrow(() -> new IllegalArgumentException("Unknown operator: " + operator));
}
// 리스트의 저장된 값들 중에서 입력받은 값보다 큰 값들만 추출하여 전달
public List <Double> getLargeNumber(List<Double> list, double number){
List <Double> nums = list.stream().filter(num -> num > number).toList();
return nums;
}
// 리스트의 저장된 값들 중 가장 높은 값 전달.
public double getMaxNumber(List<Double> list){
// mapToDouble은 스트림을 DoubleStream으로 변환해주는 메소드이다.
// 이 외에도
// mapToInt, mapToLong, mapToObject 메서드들이 있다.
return list.stream().mapToDouble(li -> li).max().orElseThrow(NoSuchElementException::new);
}
// 리스트의 저장된 값들 중 가장 작은 값 전달.
public double getMinNumber(List <Double> list){
return list.stream().mapToDouble(li -> li).min().orElseThrow(NoSuchElementException::new);
}
회고
이렇게 계산기를 1레벨부터 3레벨까지 만들어 봤는데 1레벨 2레벨은 그렇게 어련운 점은 없었지만,
3레벨을 진행하며 처음 써보는 enum과 람다, 제네릭, 스트림을 직접 활용하여 코드에 녹여내는게 정말 쉽지않았던 것 같다. 솔직히 지금 쓴 것도 그냥 맛만 보는 형식으로 쓴 것 같고... 구글링과 같은 조원분이 활용하는 것을 보고 저런식으로 쓰는구나 하고 일단 무작정 써본 것 같다.
코드를 짜면서 이슈가 몇가지 있었지만 그중의 기억에 남는 이슈
3레벨에서 클래스의 흐름은 App 클래스에서 CalculatorApp 클래스를 객체화 하여 CalculatorApp 클래스에서 받아온 값들의 유효성 검사를 하고 알맞게 들어 왔다면 ArithmeticCalculator 클래스의 getter 메서드와 setter 메서드를 활용하여 값을 컬렉션 필드에 저장 시키거나 값을 받아와서 그 값을 OperatorType의 메소드를 실행하며 같이 넘겨주는 식으로 만들었다.
처음에는 리스트의 저장된 값들 중에서 입력받은 값보다 큰 값들만 출력하기 위하여
App 클래스에서 입력받은 값을 CalculatorApp 클래스의 메서드를 호출하며 같이 넘겨준 후 그 값을 OperatorType의 넘겨주어 거기서 비교하고 큰 값들만 다시 리턴해주는 식으로 만들었었다.
// CalculatorApp 클래스
public void getLargeNumber(double number) {
List <Double> largeNumber = operator.getLargeNumber(number);
for(double num : largeNumber){
System.out.println(num);
}
}
// OperatorType 클래스
public List <Double> getLargeNumber(double number){
ArithmetiCalculator <Double> arCalcu = new ArithmetiCalculator<>();
List <Double> list = arCalcu.getArrayList();
List <Double> nums = list.stream().filter(num -> num > number).toList();
return nums;
}
// ArithmeticCalculator 클래스
public List<T> getArrayList() {
return arrayList.stream().toList();
}
처음에 내가 짠 코드이다. 근데 문제는 여기서 발생했다 배열은 넘어오긴하지만 빈 배열이 넘어온다는 점이였다.
처음에는 stream도 처음 써보고 하니까 저렇게 쓰는게 아닌가..? 하고 스트림이 아닌 그냥 for문을 돌리면서도 해봤지만 역시 빈배열이 넘어왔다 그러다가 문제점을 찾았는데
public List <Double> getLargeNumber(double number){
ArithmetiCalculator <Double> arCalcu = new ArithmetiCalculator<>();
List <Double> list = arCalcu.getArrayList();
List <Double> nums = list.stream().filter(num -> num > number).toList();
return nums;
}
이녀석이 문제였다. 여기서 ArithmetiCalculator 클래스의 객체를 새로 만들면서 그 객체를 통해 리스트의 저장된 데이터들을 가져오려는게 문제였다. 저장된게 없는데 어떻게 가져와... 그래서 코드를 수정했다.
// CalculatorApp 클래스
private final ArithmeticCalculator<Double> doubleCalculator = new ArithmeticCalculator<>();
public void getLargeNumber(double number) {
List<Double> list = doubleCalculator.getArrayList();
List<Double> lageNumbers = operator.getLargeNumber(list, number);
int num = 1;
for (double li : lageNumbers) {
System.out.println(num + ". " + li);
num++;
}
}
// OperatorType 클래스
public List <Double> getLargeNumber(List<Double> list, double number){
List <Double> nums = list.stream().filter(num -> num > number).toList();
return nums;
}
// 이건 동일
public List<T> getArrayList() {
return arrayList.stream().toList();
}
처음과 다르게 이번에는 CalculatorApp 클래스에서 getLargeNumber메서드에 앱 클래스에서 받아온 숫자만이 아닌 List <Double> 형태의 list도 같이 보내주었다
CalculatorApp 클래스에서는 getArrayList() 메서드로 지금까지 저장시킨 데이터들을 리스트 형태로 받아올 수 있었다.
왜 그런지 생각해보니 CalculatorApp 클래스 필드영역에 ArithmeticCalculator 클래스가 이미 인스터화되어있고. 계산기를 진행하며 나오는 값들을 ArithmeticCalculator을 인스터화 시킨 doubleCalculator 을 통해 저장시키고 있었기 때문에 getArrayList() 메소드도 doubleCalculator을 통하여 호출하였기 때문에
리스트형식으로 잘 가져온다. 다음에도 이와 같이 빈 배열이 넘어온다던가 비슷한 경우가 발생한다면 클래스 인스턴스화를 엉뚱하게 하여 재할당시켰는지부터 확인해 봐야겠다.
'TIL' 카테고리의 다른 글
[TIL] 9월 13일 (1) | 2024.09.13 |
---|---|
[TIL] 숫자 야구 게임 만들기 (2) | 2024.09.12 |
[TIL] 계산기 3레벨 해보기 (6) | 2024.09.09 |
[TIL] 오늘은 금요일 ! (1) | 2024.09.06 |
[TIL] 오늘 (7) | 2024.09.04 |