강의/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

https://docs.oracle.com/javase/tutorial/datetime/iso/overview.html

 

 

# 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) {
    ...
  }

}