강의/Java
[실전자바] 각종 클래스
atmosg
2024. 8. 4. 10:16
1. String 클래스
# 사용이유
- 원래라면 문자'열'은 기본형 char를 배열로 만들어 char[] 형태로 보관해야한다
- 그러나 char[]는 사용도 불편하고 관리도 어렵다
- 때문에 문자열을 보다 쉽게 처리하기 위해 도입됨
- 덕분에 지금의 String str = "abc" 같은 간단한 선언이 가능해진 것
# 클래스 구조
- String 클래스는 내부적으론 char[] 를 사용해 문자열을 보관함
- 즉, 사람이 더 쓰기 편하게 만들었을뿐 근본적으론 문자열을 다루기 위해 char[]를 사용하는건 그대로인 것
* Java9 버전 이후부턴 char[] → byte[] 배열에 보관(메모리 효율성)
# 특이한 선언과 연산
- String 클래스로 만들어진 값은 원칙적으론 참조형이다
- 따라서 변수 생성시 String str = new String("abc")와 같이 선언해야 한다
- 또한, "hello" + "world" 같은 직접적인 연산도 불가능하다
- 그러나 문자열은 밥먹듯이 쓰기 때문에 다루기 편하도록 특이한 연산 기능을 제공한다
ㄴ (선언) String str = "abc" → 내부적으로 String str = new String("abc")로 변환해 수행
ㄴ (연산) "hello" + "world" → 기본형과 동일하게 "hello world"로 처리
# String 비교와 문자열 풀
- String은 참조형이기 때문에 같은 문자일지라도 동일성(==) 비교시 false를 반환한다
- 그러나 문자열 리터럴로 선언된 경우에 한해 동일성(==) 비교시 true를 반환한다
- 위와 같은 결과가 나오는 이유는 자바가 문자열 풀을 사용하기 때문
- 여하튼 문자열 비교시 헷갈릴 수 있으므로 동등성(equals)를 사용해야함
# 문자열 풀(String pool)
- 프로그래밍에서 풀(Pool)이란 공용 자원을 모아둔 곳을 뜻함
- 따라서 문자열 풀이란 '공용 문자열'을 모아둔 곳
- 자바는 실행 시점에 코드를 평가하는 과정에서 문자열 리터럴로 선언된 값은 문자열 풀에 보관
- 참고로 문자열 풀은 힙 영역을 사용하며, 문자열 풀에서 문자 검색시 해시 알고리즘을 사용함
# 불변 클래스
- String 클래스는 불변으로 설계되어 있다
- 그 이유는 문자열 풀을 통해 최적화하는데 만약 값이 바뀔 수 있으면 의도치않은 사이드 이펙트를 유발하기 때문
- 즉, 누군가에 의해 공유 자원이 변경/훼손될 경우 애꿎은 다른 변수값까지 변경돼버리기 때문
# StringBuilder (가변String)
- String 클래스는 불변이라 대부분의 메서드가 연산 후 새로운 String을 반환한다
- 즉, "A" + "B" + "C" + ... 처럼 문자의 덧셈/변경 시 연산을 위해 String 객체가 계속 생성되므로 비효율적
ㄴ new String("A") + new String("B") + new String("C")
ㄴ new String("AB") + new String("C")
ㄴ new String("ABC")
ㄴ 물론 최신 버전의 자바에선 이 과정도 내부적으로 최적화해서 수행하긴함 - 이 문제를 해결하기위해 나온게 가변 String인 StringBuilder 클래스
# 자바의 String 최적화
- 요근래엔 "A"+"B"+"C"같은 단순 리터럴 연산은 자바가 알아서 최적화하기 때문에 메모리 걱정을 할 필요X
- 그럼에도 불구하고 반복문內 문자열 덧셈 같은 연산은 아직까지 최적화가 되지 않고있음
- 때문에 간단한 연산에선 평소 하듯이 하면되지만, 일부 경우에 한해선 StringBuilder를 이용하는게 더 좋다
ㄴ 반복문내 문자열 연결
ㄴ 조건문을 통해 동적으로 문자열 조합
ㄴ 복잡한 문자열의 특정 부분 변경
ㄴ 매우 긴 대용량 문자열 취급시
# StirngBuffer
- StringBuilder와 동일한 기능을 수행하지만 내부적으로 동기화처리를 해놓은 클래스
- 멀티 스레드 상황에서 안전한 사용을 위해 고안됨
- 다만 동기화 오버헤드로 인해 성능이 느림
2. 래퍼 클래스
# 래퍼 클래스가 왜 필요할까
- 래퍼 클래스 : 기본형 데이터를 감싸서(Wrap) 객체로 다루기 위해 사용하는 클래스
- 기본형(Primitive type)은 객체가 아니라 객체 지향 프로그래밍의 장점을 못써먹을 때가 있음
ㄴ 기본형은 객체가 아니라 메서드 제공이 없음
ㄴ 컬렉션 프레임워크, 제네릭 사용도 못함
ㄴ null값도 못가짐 (데이터가 '없는' 상태를 표현할 수가 없음) - 대신 기본형 대비 연산 속도는 느림
# 오토 박싱/언박싱 (자바 5)
- 매번 박싱/언박싱 변환하기 너무 귀찮아서 내부적으로 박싱/언박싱을 자동으로 해줌
ㄴ Integer boxed = 10;
ㄴ int primitive = boxed; - 박싱(Boxing) : 기본형을 래퍼 클래스로 만드는 작업을 박싱이라고 함
- 언박싱(Unboxing) : 래퍼 클래스에 저장된 값을 기본형으로 꺼내는 작업을 언박싱이라고 함
3. 열거형(Enum) 클래스
# 사용이유
- 열거란 어떤 항목을 나열한 것을 말하며, Enum Type이란 '고정된 어떤 항목들이 나열된 자료형'
- 즉, 일련의 명명된 상수들의 집합을 정의한 자료형
- 그런데 사실 상수를 정의하고 쓰는 용도 정도라면 String을 사용해도 충분하긴 하다
- 그런데 문제는 코드 작성이 아닌, 사용 시 문제 발생 소지가 많다는 점임
- 예를 들어, 메서드에서 String을 입력받는다면 사용자가 무슨 값을 전달할지 알수가 없음
- 즉, 값을 제한할 수도 없을뿐더러 컴파일 과정까진 아무 문제가 없다가 런타임까지 가서야 문제가 발생함(이러면 디버깅이 어려워짐)
public int discount(String grade) {
if (grade.equals("VIP") return 0.2
if (grade.equals("GOLD") return 0.05
}
discount("vip") // 아니 대문자로 입력해야된다니까?
discount("SILVERR") // R 두번은 오타아님?
# 발상의 시작(타입 안전 열거형 패턴)
- String을 입력받아 작업을 수행하려니 문제의 소지가 많다면 클래스를 받아 처리하도록 만들면 되지 않을까?
- 이러면 정해진(제한된) 객체만 사용하므로 타입 안정성(컴파일 시점에 오류추적 가능)과 일관성이 보장됨
- 이 패턴이 너무 괜찮다보니 자바에선 이를 열거형이란 타입으로 동일 기능을 제공하기 시작
public int discount(ClassGrade grade) {
if (grade == ClassGrade.VIP) return 0.2
if (grade == ClassGrade.GOLD) return 0.05
}
class ClassGrade {
public static final ClassGrade VIP = new ClassGrade();
public static final ClassGrade GOLD = new ClassGrade();
private ClassGrade() {} // 거 객체 만들어 쓸 생각 마시오
}
# Enum 주요 메서드
- values() // Enum에 정의된 상수들을 반환
- ordinal() // Enum 상수들의 순서값을 반환
- valueOf(String) // 문자를 Enum상수로 바꿔줌
- name() // Enum에 정의된 상수를 문자로 반환
# Enum은 클래스다
- Enum을 정의할때 public enum Grade { ... } 같이 선언하므로 클래스와 별개의 것이라고 착각할 수 있음
- 그러나 실체는 enum클래스를 자동으로 상속받는 클래스일 뿐임
public class Grade extends enum { ... } - 즉, 타입 안전 열거형 클래스와 정확히 동일함
- 따라서 필드나 메서드도 필요하다면 얼마든지 추가가 가능함
4. 날짜/시간 관련 클래스
# 날짜/시간 라이브러리가 왜필요함
- 날짜/시간 계산이 겉보기엔 쉬워보여도 막상 해보면 개어려움
ㄴ 생각보다 일정하지 않은 항목들이 많기 때문.
ㄴ 윤년, Time Zone(UTC, KST, ...), 써머타임(DST) 기타 등등 - 이 시간처리 때문에 자바는 여러 패키지를 도입했는데 다 미덥지 못했음
- 그러다가 JDK 1.8버전에 이르러서야 쓸만한 java.time 패키지가 도입됨
ㄴ 해당 패키지 안에 LocalDate, LocalTime, LocalDateTime, ZondedDateTime, Instant 클래스 등이 포함됨
# 패키지 살펴보기
- 모든 날짜 관련 클래스는 불변
- 일반적인 연월일시 표기하고싶다 → LocalDateTime, LocalTime, LocalDate
- 세계 각 지역별 시간이 중요하다 (Offset : UTC ± 몇시간인지 나타내는 지표)
ㄴ 써머타임 고려O → ZonedDateTime
ㄴ 써머타임 고려X → OffsetDateTIme - 무슨 요일인지 표기하고싶다 → DayOfWeek
- 현재가 1970.01.01 00:00:00UTC 로부터 몇 나노초인지 알고싶다 → Instant
- 시간 간격을 계산하고싶다
ㄴ 몇년 / 몇개월 / 며칠 차이인지 → Period
ㄴ 몇 시간 / 몇 분 / 몇 초 차이인지 → Duration
# LocalDateTime
- 내부에 LocalDate와 LocalTime 필드가 선언돼있음
- LocalDateTime 객체 생성관련 메서드
ㄴ LocalDateTime.now()
ㄴ LocalDateTime.of(...params)
ㄴ LocalDateTime.of(localDate, localTime) - 날짜, 시간 필드 뽑기
ㄴ localDateTime.toLocalDate() // 날짜만 뽑기
ㄴ localDateTime.toLocalTime() // 시간만 뽑기 - 날짜, 시간 더하기
ㄴ localDateTime.plusDays(day)
ㄴ localDateTime.plusYears(year) - 기타 유용한 메서드
ㄴ localDateTime.isBefore(x) // x보다 이전 시간인가?
ㄴ localDateTime.isAfter(x) // x보다 이후 시간인가?
ㄴ localDateTime.isEuqals(x) // x와 동일한 시간인가? (서울 09시는 00UTC와 동일함)
# ZoneId
- 타임존 관련 클래스로 지역과 UTC와의 시간차 정보를 갖고있음
- 써머타임 정보도 함께 포함
- 타임존 목록 및 값 확인
ㄴ ZoneId.getAvailableZoneIds()
ㄴ ZoneId.systemDefault()
ㄴ ZoneId.of("Asia/Seoul")
ㄴ ZonedDateTime.of(localDateTime, ZoneId.of("Asia/Seoul")) - 타임존 변환
ㄴ localDateTime.withZoneSameInstant(ZoneId.of("UTC"))
# OffsetDateTime
- 타임존은 없고 UTC와의 사간차 정보만 가짐
- 오프셋타임 생성
ㄴ OffsetDateTime.now()
ㄴ OffsetDateTime.of(localDateTime, ZoneOffset.of("+01:00"))
# Instant
- UNIX시간(Epoch 시간 이라고도 부름)을 나타내는 클래스
- 사람이 읽고 이해하려고 만든 시간이라기 보단 기계적 처리를 위해 사용되는 시간임
ㄴ 로그 기록
ㄴ DB/트랜잭션 타임스탬프
ㄴ 서버 간 시간 동기화 - Instant 생성 및 계산
ㄴ Instant.now()
ㄴ Instant.from(TemporalAccessor) // LocalDateTime은 UTC가 아니라서 못씀
ㄴ Instant.ofEpochSecond(0) // 1970-01-01 00:00:00Z
ㄴ instant.plusSeconds() - Epoch 시간 출력
ㄴ instant.getEpochSecond() - UNIX 시간 : 1970.01.01 00:00:00UTC로부터의 경과 시간을 초로 환산해 정수로 나타낸 것. 유닉스 계열 운영 체제나 기타 운영 체제, 여러 파일 형식에서 사용된다
# Period, Duration
- 두 날짜/시간 사이의 간격을 계산하기 위한 클래스
ㄴ 전자는 연월일 차이의 계산용
ㄴ 후자는 시분초 차이의 계산용 - 기간 차이 설정
ㄴ Period.of(year, month, days) // 날짜 차이 설정
ㄴ Period.ofDays()
ㄴ Period.ofMonths()
ㄴ Period.ofYears() - 두 날짜 사이의 차이 계산
ㄴ Period.between(localDate1, localDate2) - Duration 메서드도 Period와 대동소이함
# 시간 단위에 대한 정의
- 시간이란 연월일, 시분초 등의 여러 단위로 구성된 정보
- 따라서 연, 월, 일, 시, 분, 초 등의 각 단위를 정의해줄 무언가가 필요
- 이러한 시간 단위에 대한 정의를 나타내는 인터페이스가 TemporalUnit
- 그리고 TemporalUnit을 구현하는 주요 구현체가 ChronoUnit임 (참고로 enum임)
- 날짜 단위 관련
ㄴ ChonoUnit.DAYS, WEEKS, MONTHS, YEARS, DECADES, CENTURIES, MILLENNIA
ㄴ ChronoUnit.ERAS, FOREVER - 시분초 단위 관련
ㄴ ChronoUnit.NANOS, MICROS, MILLILS, SECONDS, MINUTES, HOURS - 주요 메서드
ㄴ chronoUnit.between()
ㄴ chronoUnit.isDateBased()
ㄴ chronoUnit.isTimeBased()
ㄴ chronoUnit.isSupportedBy(Temporal)
ㄴ chronoUnit.getDuration()
# 각 시간 단위별 값에 대한 표기
- TemproalUnit이 각 시간의 정의를 나타내는 인터페이스
- 해당 정의에 따라 실제 값을 나타내기 위한 인터페이스가 TemproalField임
- 그리고 TemporalField를 구현하는 주요 구현체가 ChronoField임 (참고로 enum임)
- 연월일 관련 필드
ㄴ ChronoFiled.ERA, YEAR_OF_ERA, YEAR, EPOCH_DAY
ㄴ ChronoFiled.MONTH_OF_YEAR, PROLEPTIC_MONTH
ㄴ ChronoFiled.DAY_OF_WEEK, DAY_OF_MONTH, DAY_OF_YEAR, EPOCH_DAY
ㄴ ChronoFiled. ALIGNED_DAY_OF_WEEK_IN_MONTH, ALIGNED_DAY_OF_WEEK_IN_YEAR - 시분초 관련 필드
ㄴ ChronoFiled.HOUR_OF_DAY, CLOCK_HOUR_OF_DAY, HOUR_OF_AMPM
ㄴ ChronoFiled.CLOCK_HOUR_OF_AMPM, MINUTE_OF_HOUR, SECOND_OF_MINUTE
ㄴ ChronoFiled.NATNO_OF_SECOND, MICRO_OF_SECOND, MILLI_OF_SECOND - 기타 필드
ㄴ ChronoFiled.AMPM_OF_DAY, INSTANT_SECONDS, OFFSET_SECONDS - 주요 메서드
ㄴ chronoFiled.getBaseUnit()
ㄴ chronoFiled.getRangeUnit()
ㄴ chronoFiled.isDateBased()
ㄴ chronoFiled.isTimeBased()
ㄴ chronoFiled.range()
# 시간 조작하기
- Temporal, TemporalAccessor 인터페이스를 뜯어보면 시간의 조회 및 조작을 위한 메서드가 이미 존재
- 이를 구현하는 온갖 구현체들(LocalDateTime, LocalDate, Instant, ...)은 당연히 이 메서드들을 사용할 수 있음
- 따라서 시간 조작시 쓸데없는짓 하지 말고 이미 마련된 메서드를 사용할 것
- 시간 조회하기
ㄴ temporal.get(TemporalField field) - 조회관련 주의사항
ㄴ LocalDate 시간 정보가 없어서 hour, min 같은 조회가 안됨
ㄴ 즉, 경우에 따라 조회가 불가능한 경우도 있으므로 이를 확인하는 isSupported 메서드도 존재함
ㄴ TemporalAccessor.isSupported(TemporalField field)
ㄴ Temporal.isSupported(TemporalUnit unit) - 시간 더하기
ㄴ temporal.plus(long amountToAdd, TemporalUnit unit)
ㄴ temporal.plus(TemporalAmount amount)
ㄴ temporal.plusYears, plusMonths, ... - 시간 변경하기
ㄴ temporal.with(TemporalField field, long newValue) - 매우 특별한 조작
ㄴ 오는 금요일, 이번 달 마지막 일요일의 날짜 같은 매우 어려운 조회를 위한 TemporalAdjusters 클래스가 존재
ㄴ temporal.with(with(TemporalAdjuster adjuster)와 함께쓰며 자세한건 아래 코드 참고
※ 관련 코드
더보기
# 시간 조회
LocalDateTime now = LocalDateTime.now();
int year = now.get(ChronoField.YEAR);
int month = now.get(ChronoField.MONTH_OF_YEAR);
int day = now.get(ChronoField.DAY_OF_MONTH);
int dayOfWeek = now.get(ChronoField.DAY_OF_WEEK); // 무슨요일(1~7)
# 시간 조작
// 현재에서 10년을 더하는 방법들
LocalDateTime now = LocalDateTime.now();
LocalDateTime py1 = now.plus(10, ChronoUnit.YEARS);
LocalDateTime py2 = now.plusYears(10);
Period of10Years = Period.ofYears(10);
LocalDateTime py3 = now.plus(of10Years);
# 시간 조작
LocalDateTime now = LocalDateTime.now();
LocalDateTime cy = now.with(ChronoField.YEAR, 2025);
// 오는 금요일
LocalDateTime nextFriday = now.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
// 이번 달 마지막 일요일
LocalDateTime lastSundayInThisMonth = now.with(TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY));
# 날짜와 시간 파싱/포매팅
- 파싱 : 문자열을 날짜/시간 데이터로
- 포매팅 : 날짜/시간 데이터를 문자열로
- 포매팅 패턴 공식 사이트
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter1 = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일");
String formatted = now.format(formatter1);
String date = "2024년 08월 19일 08시 30분 30초";
DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분 ss초");
LocalDateTime parsed = LocalDateTime.parse(date, formatter2);
5. 중첩 클래스
# 중첩 클래스(Nested Class)
- 클래스 안에 정의된 클래스를 말함
- 종류 : 크게 두 가지로 구분됨 (정적 중첩 클래스, 내부 클래스)
- 내부 클래스는 다시 3가지로 분류됨
ㄴ 내부 클래스 (inner class)
ㄴ 지역 클래스 (local class)
ㄴ 익명 클래스 (anonymous class)
# 왜 사용하는가
- 특정 클래스가 다른 클래스 안에서만 사용될 경우 논리적 그룹화를 위해
- 외부에서 사용될 필요가 없는 클래스를 숨겨서 불필요한 노출을 줄임(캡슐화)
※ 예시 코드
더보기
class Outer {
// static nested class (정적 중첩 클래스)
static class StaticNested { ... }
// 내부 클래스
class Inner { ... }
// 지역 클래스
public void process() {
Local local = new Local();
class Local { ... }
}
// 익명 클래스
public void print() {
Process p = new Process() {
@Override
void run() {...}
}
p.run();
}
}
interface Process() {
void run();
}
6. 제네릭
# 제네릭이란
- 메서드나 컬렉션 등에서 컴파일 시의 타입체크를 해주는 기능
- 핵심은 사용할 타입을 미리 결정하지 않는다는 점
- 즉, 타입 매개변수를 전달받아 사용 시점에 타입을 결정함
ㄴ Generic<T> <-- T를 전달받아 사용
# 왜 사용하나
- 타입 안정성
- 타입 체크과 형변환 생략 가능 (코드 간결)
# 제네릭과 컴파일 (타입 이레이저)
- 제네릭은 실제 존재하는 타입이라기보단 컴파일시 타입 체크를 위한 임시 데이터에 가까움
- 따라서 컴파일 이후 생성된 자바 바이트코드(.class)상엔 제네릭 관련 정보가 사라짐
- 물론 제네릭 정보가 사라진다해도 전달받은 타입은 존재해야 하기 때문에 해당 값들에 대해선 형변환 코드를 넣어버림
- 쉽게 설명하면 아래와 같이 취급됨 (예시일뿐 실제는 아님)
// 컴파일 전
class Box<T> {
T content;
T getcontent() {
return this.content;
}
}
GenericBox box = new GenericBox<Integer>();
Integer content = box.getContent();
// 컴파일 후
// 타입 파라미터는 전부 제거
// 타입이 정해진 결과값은 해당 타입으로 형변환(다운캐스팅)
class Box {
Object content;
Object getcontent() {
return this.content;
}
}
GenericBox box = new GenericBox();
Integer content = (Integer) box.getContent();
# 제네릭 메서드
- 제네릭 타입은 클래스에 사용 GeneericClass<T>
- 제네릭 메서드는 클래스 전제가 아니라 메서드 단위에 제네릭을 적용한 것을 말함
ㄴ <T,V> V method(T obj);
ㄴ 매개변수 타입 T를 전달받아서 V 타입을 리턴하는 제네릭 메서드임 - 우선순위 따지기
// 제네릭 메서드에서 받는 타입 T는 제네릭 타입(클래스)에서 받는 T와 무관함
// 물론 아래처럼 중복되고 모호한 이름으로 만든것 자체가 잘못이긴함
class GenericType<T> {
public <T> T genericMethod(T obj) {
...
}
}