[Java] 자바의 신 학습 정리 VOL.1 기초 문법편
1. Java 레이블(label)
1.1. Java 레이블(label) 개념
- Java의 레이블(label)은 문(statement)에 이름을 붙여서 중첩된 제어 흐름을 보다 명시적으로 제어할 수 있게 해 주는 기능이다. 레이블을 붙이면 break나 continue문에서 특정 위치로 바로 탈출하거나 반복을 건너뛸 수 있다.
- 또한, Java에서는 for, 향상된 for(enhanced for), while, do-while 등 모든 종류의 루프문 앞에 레이블을 사용할 수 있다.
- 레이블은 식별자(identifier) 다음에 콜론(:)을 붙여 선언하며, 뒤따르는 하나의 문(statement)에 이름을 부여한다.
- JLS에 따르면 모든 문(statement)은 레이블을 가질 수 있고, 레이블은 break 또는 continue 문 안에서만 참조될 수 있다.
- 레이블은 루프뿐 아니라 블록({}), if문, switch문 등 모든 형태의 문(statement)에 붙일 수 있다. 다만, break문은 switch와 루프에서만 동작한다.
- continue문은 레이블이 지정된 루프 내부에서만 사용할 수 있으며, 해당 레이블이 붙은 루프의 “다음 반복”을 수행한다.
1.2. 사용 예시
- 기본 사용 방법
- if 조건에 부합되면 startLabel 다음 loop 부터 시작된다.
public class ControlLabel{
public static void main(String[] args) {
ControlLabel control = new ControlLabel();
control.printTimesTable();
}
public void printTimesTable(){
startLabel:
for(int level =2; level<10; level++){
for(int unit=1;unit<10;unit++){
for(int one=1; one<10; one++){
if(one==4) continue startLabel;
System.out.print(level+"*"+unit+"+"+one+"="+level*unit+one+" ");
}
System.out.println();
}
System.out.println();
}
}
}
- 확장 사용 방법
- if 조건에 부합되면 startLabel 다음 loop 부터 시작된다.
public class ControlLabel{
public static void main(String[] args) {
ControlLabel control = new ControlLabel();
control.printTimesTable();
}
public void printTimesTable(){
for(int level =2; level<10; level++){
startLabel:
for(int unit=1;unit<10;unit++){
for(int one=1; one<10; one++){
if(one==4) continue startLabel;
System.out.print(level+"*"+unit+"+"+one+"="+level*unit+one+" ");
}
System.out.println();
}
System.out.println();
}
}
}
2. static 초기화 블록(static initializer block)
2.1. 정의 및 문법
- 클래스가 메모리에 로드될 때 한 번만 실행되는 특수한 코드 블록으로, 주로 복잡한 정적(static) 데이터의 초기화용으로 사용된다. 내부에 선언된 변수는 일반 메서드 내의 로컬 변수(local variable)와 같아, 타입을 반드시 명시해야 하며(Java10부터는 var 키워드를 통한 타입 추론 가능) 정적 필드(클래스 변수)와는 구분되어 취급된다.
2.2. 사용 예시 및 장점
- 복잡한 상수 맵 초기화:
static Map<String,Integer> codeMap = new HashMap<>();
static {
codeMap.put("A", 1);
codeMap.put("B", 2);
// …
}
2.3. static 블록 내 변수 선언 및 타입
- static 블록 안에서 선언된 변수는 로컬 변수로 취급되어, 블록 밖에서 접근할 수 없으며, 정적 필드가 아니다.
- 로컬 변수이므로 타입을 명시해야 하며, Java 10 이상에서는 var 키워드를 통해 타입 추론만으로도 선언이 가능하다.
- var는 로컬 변수에만 적용되며, 멤버 변수, 메서드 파라미터, 반환 타입 등에는 사용할 수 없다.
static {
int x = 10; // 명시적 타입 선언
var y = "Hello"; // Java 10+에서 가능
// var z; // 초기화 없는 var 선언 불가 → 컴파일 오류
}
2.4. var 사용 예시 코드
public class VarInStaticBlock {
// static 초기화 블록
static {
// var로 선언된 로컬 변수: 각각 String과 int 타입으로 추론
var message = "안녕하세요, static 블록입니다!";
var count = 3;
// 반복문에서도 var 사용 가능 (로컬 변수이므로 문제 없음)
for (var i = 1; i <= count; i++) {
System.out.printf("[%d] %s%n", i, message);
}
}
public static void main(String[] args) {
// 아무 동작 없이 static 블록만 실행된 후 종료
}
}
3. 참조형의 정의
- 클래스 타입(class types), 인터페이스 타입(interface types), 배열 타입(array types) 그리고 특별한 null 타입(null type)을 포함한다.
3.1. 참조형의 주요 종류
3.1.1. 클래스 타입 (Class Types)
- String, Integer, 사용자 정의 클래스(MyClass) 등 클래스로 선언된 모든 타입이 여기에 속한다.
String s = "Hello"; // String 클래스 타입
MyClass obj = new MyClass(); // 사용자 정의 클래스 타입
3.1.2. 인터페이스 타입 (Interface Types)
- List, Runnable 등 인터페이스로 선언된 타입. 변수는 해당 인터페이스를 구현(implement)한 객체를 참조할 수 있다.
List<String> list = new ArrayList<>(); // ArrayList는 List 인터페이스 구현체
Runnable r = () -> System.out.println("run"); // 람다도 Runnable 구현
3.1.3. 배열 타입 (Array Types)
- int[], String[], MyClass[][] 등 배열도 참조형입니다. 배열 리터럴 또는 new 연산자로 생성된다.
int[] nums = new int[5];
String[] names = {"A", "B", "C"};
3.1.4. 타입 변수 (Type Variables)
- 제네릭(Generic)으로 선언된 타입 매개변수(T, E 등)도 참조형으로 취급된다.
class Box<T> {
private T content;
public void set(T content) { this.content = content; }
public T get() { return content; }
}
4. 매개변수로의 배열 사용 방법
4.1. 배열 형식
class arrayTest {
public static void main(String[] args) {
String[] myNames = {"Alice", "Bob", "Charlie"};
arrayTest printNames = new arrayTest();
printNames.printNames(myNames); // 배열을 전달
}
public void printNames(String[] names) {
for (String name : names) {
System.out.println(name);
}
}
}
4.2. 가변 인자 형식
class arrayTest{
public static void main(String[] args) {
arrayTest printNames = new arrayTest();
printNames.printNames("Alice", "Bob", "Charlie"); // 배열처럼 쓰지만 내부적으로 배열로 처리됨
}
public void printNames(String ... names) {
for (String name : names) {
System.out.println(name);
}
}
}
- 가변 인자를 사용할 때 “하나만 가능하며, 반드시 마지막에 위치해야 한다.
- 이 규칙은 메서드 시그니처의 모호성을 방지하기 위해 존재한다.
5. 패키지
5.1. 기본 사용 방법
- 프로젝트의 root 폴더 위치에서 상대경로를 package로 선언해줘야 하며, 해당 폴더의 java 파일은 패키지당 하나만 선언할 수 있는 public으로 선언된 대표 클래스의 이름과 동일해야 한다.
// test/game/pick/A.java
package test.game.pick;
public class A {
public static void greet() {
System.out.println("Hi from A");
}
}
- 패캐지를 사용하려면, 해당 경로와 파일명을 import를 이용해 선언해야 한다.
// Main.java
import test.game.pick.A;
public class Main {
public static void main(String[] args) {
A.greet(); // ✅ 정상 작동
}
}
5.2. 한 패키지에 여러 클래스 정의하기
- zoo 클래스는 animals 폴더에 있다.
- 파일의 이름이 Zoo.java이기 때문에 대표 클래스는 public으로 선언된 class Zoo가 된다.
- 파일 이름과 다른 클래스가 public을 이용해 선언되어 있으면 컴파일 에러가 발생한다.
- 대표 클래스 외의 클래스들은 접근제어자가 없다. 즉 같은 패키지 내에서만 접근이 가능하다.
// File: Zoo.java
package animals;
public class Zoo {
public static void main(String[] args) {
Lion lion = new Lion();
Tiger tiger = new Tiger();
lion.roar();
tiger.growl();
}
}
class Lion {
void roar() {
System.out.println("Roar!");
}
}
class Tiger {
void growl() {
System.out.println("Grrrr!");
}
}
5.3. import static 문법 정리
형태 | 설명 |
import static 패키지.클래스.변수명; | 특정 static 변수만 import |
import static 패키지.클래스.메서드명; | 특정 static 메서드만 import |
import static 패키지.클래스.*; | 해당 클래스의 모든 static 멤버를 import |
- 사용 예제
// MyUtils.java
package util;
public class MyUtils {
public static final String APP_NAME = "MyApp";
public static void sayHello() {
System.out.println("Hello from MyUtils!");
}
}
- 방법 1: 각각 static import
import static util.MyUtils.APP_NAME;
import static util.MyUtils.sayHello;
public class Main {
public static void main(String[] args) {
System.out.println(APP_NAME);
sayHello();
}
}
- 방법 2: 통합 static import
import static util.MyUtils.*;
public class Main {
public static void main(String[] args) {
System.out.println(APP_NAME); // OK
sayHello(); // OK
}
}
5.4. 같은 패키지 내의 클래스들의 import
- 같은 패키지 내의 클래스들은 서로를 import 없이 사용할 수 있다.
5.4.1. 패키지가 같은 경우
// 파일: Dog.java
package com.example;
public class Dog {
public void bark() {
System.out.println("멍멍!");
}
}
// 파일: Main.java
package com.example;
public class Main {
public static void main(String[] args) {
Dog dog = new Dog(); // import 필요 없음
dog.bark();
}
}
5.4.2. 패키지가 다른 경우
// 파일: Dog.java
package com.animals;
public class Dog {
public void bark() {
System.out.println("멍멍!");
}
}
// 파일: Main.java
package com.example;
import com.animals.Dog; // 이게 필요함!
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.bark();
}
}
6. 상속(abstract)
6.1. 상속 기본 개념
6.1.1 슈퍼클래스와 서브클래스
- 슈퍼클래스(Superclass): 필드와 메서드를 제공하는 기반 클래스
- 서브클래스(Subclass): 슈퍼클래스의 기능을 상속받아 확장하는 파생 클래스
6.1.2. “is‑a” 관계
- 서브클래스는 슈퍼클래스의 하위 개념으로, “서브클래스는 슈퍼클래스의 일종이다(is‑a)” 관계를 형성한다.
6.1.3 단일 상속
- Java에서는 클래스 다중 상속을 허용하지 않으며, extends 키워드로 하나의 슈퍼클래스를 지정할 수 있다.
// Vehicle.java
public class Vehicle {
public void startEngine() {
System.out.println("엔진 시동");
}
}
// Car.java
public class Car extends Vehicle {
public void openTrunk() {
System.out.println("트렁크 열기");
}
}
6.2. extends 키워드
- extends 키워드를 사용하여 슈퍼클래스를 상속받고, 그 멤버를 그대로 사용하거나 오버라이드할 수 있다.
- 오버라이딩을 하는 경우, 접근 제어자가 더 확대되는 것은 문제가 안되지만, 축소되는 것은 문제가 된다.
- public > protected > package-private > private
- 메소드 오버라이딩은 부모 클래스의 메소드와 동일한 시그니처(함수/변수 명)을 가지는 자식 클래스의 메소드가 존재할 경우에만 성립이 된다.
- 오버라이딩된 메소드는 부모 클래스와 동일한 리턴 타입을 가져야 한다.
- 오버라이딩 된 메소드의 접근 제어자는 부모 클래스에 있는 메소드와 달라도 되지만, 접근 권한이 확장되는 경우에만 허용된다. 접근 권한이 축소될 경우에는 컴파일 에러가 발생한다.
public class Animal {
public void eat() { System.out.println("동물이 먹습니다."); }
}
public class Dog extends Animal {
@Override
public void eat() { System.out.println("강아지가 먹습니다."); } // 오버라이드
public void bark() { System.out.println("멍멍!"); }
}
6.3. super 키워드
- super(): 서브클래스 생성자에서 슈퍼클래스 생성자를 명시 호출할 때 사용한다.
- super.field, super.method(): 서브클래스에서 숨겨진(super hidden) 멤버를 참조할 때 사용한다.
- super.super는 언어 차원에서 금지되어 있어 직접 호출할 수 없다.
- 자식 클래스의 생성자에서 첫 줄에 위치해야 한다.
- 자식 클래스의 생성자에서 super()를 호출하지 않으면, 컴파일러가 자동으로 부모 클래스의 기본 생성자를 호출한다. 하지만 부모 클래스의 다른 생성자를 호출해야 할 경우에는 반드시 명시적으로 호출해야 한다.
- 부모 클래스의 멤버 변수나 메서드를 오버라이딩한 경우, super를 사용하여 부모 클래스의 구현을 명시적으로 호출할 수 있다.
- 접근 제어 지시자:
- super를 사용하여 부모 클래스의 private 멤버에 접근할 수 없다. protected 멤버는 접근할 수 있지만, 같은 패키지가 아니면 default 멤버에는 접근할 수 없다.
- static 멤버와 super:
- super는 인스턴스 멤버에만 사용할 수 있으며, static 멤버나 static context에서는 사용할 수 없다.
public class Parent {
protected String name;
public Parent(String name) {
this.name = name;
}
public void printName() {
System.out.println("Parent: " + name);
}
}
public class Child extends Parent {
private String nickname;
public Child(String name, String nickname) {
super(name); // 슈퍼클래스 생성자 호출
this.nickname = nickname;
}
@Override
public void printName() {
super.printName(); // 슈퍼클래스 메서드 호출
System.out.println("Child: " + nickname);
}
}
6.4. 생성자(Constructor)와 상속
6.4.1. 생성자는 상속되지 않는다.
- 생성자는 클래스의 멤버가 아니므로 서브클래스에 상속되지 않는다.
6.4.2 슈퍼클래스 생성자 호출
- 서브클래스 생성자 첫 줄에 super(...)를 쓰지 않으면, 컴파일러는 파라미터 없는 슈퍼클래스 생성자를 자동으로 호출한다.
public class Base {
public Base() { System.out.println("Base() 호출"); }
public Base(int x) { System.out.println("Base(int) 호출: " + x); }
}
public class Derived extends Base {
public Derived() {
// super()가 자동 호출되어 Base() 실행
System.out.println("Derived() 호출");
}
public Derived(int x) {
super(x); // Base(int) 호출
System.out.println("Derived(int) 호출");
}
}
6.4.3 기본 생성자 자동 생성
- 클래스에 생성자를 하나도 정의하지 않으면, 컴파일러가 매개변수 없는 기본 생성자를 자동으로 추가한다 .
6.4.4 파라미터 생성자 정의 시 기본 생성자 제거
- 매개변수가 있는 생성자를 하나라도 정의하면, 기본 생성자는 자동 생성되지 않으므로 명시적으로 정의해야 한다.
public class Foo {
public Foo(int x) { /*...*/ }
// public Foo() {} // 없으면 기본 생성자는 자동으로 사라집니다.
}
6.5. 다형성이란?
- Java에서는 같은 타입의 참조 변수가 다양한 실제 객체를 참조할 수 있도록 해준다. 즉, 하나의 인터페이스나 부모 클래스를 기반으로 여러 하위 클래스의 객체를 다룰 수 있다.
6.5.1. 상속과 오버라이딩을 통한 다형성
- myAnimal1과 myAnimal2는 모두 Animal 타입이지만, 실행 시점에 실제 객체의 메서드가 호출됩니다. → 런타임 다형성
class Animal {
void sound() {
System.out.println("동물이 소리를 냅니다.");
}
}
class Dog extends Animal {
void sound() {
System.out.println("멍멍!");
}
}
class Cat extends Animal {
void sound() {
System.out.println("야옹~");
}
}
public class Main {
public static void main(String[] args) {
Animal myAnimal1 = new Dog(); // Animal 타입이지만 실제 객체는 Dog
Animal myAnimal2 = new Cat(); // Animal 타입이지만 실제 객체는 Cat
myAnimal1.sound(); // 멍멍!
myAnimal2.sound(); // 야옹~
}
}
6.5.2. 인터페이스 기반 다형성
- 인터페이스를 사용하면 더 유연한 다형성이 가능해져서, 다양한 구현체를 동일한 타입으로 처리할 수 있다.
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() {
System.out.println("원을 그립니다.");
}
}
class Rectangle implements Shape {
public void draw() {
System.out.println("사각형을 그립니다.");
}
}
public class Main {
public static void main(String[] args) {
Shape s1 = new Circle();
Shape s2 = new Rectangle();
s1.draw(); // 원을 그립니다.
s2.draw(); // 사각형을 그립니다.
}
}
6.5.3. 부모 클래스의 변수에 자식 클래스 객체 할당 하는 방법
- 이게 가능한 이유는 자식 클래스는 부모 클래스의 일종이기 때문이다.
- 즉, 자식 클래스는 부모 클래스의 모든 속성과 기능을 상속받기 때문에, 자식 객체는 부모 타입으로도 볼 수 있다.
부모클래스 변수 = new 자식클래스();
6.5.3.1. 코드로 예시 보기
class Animal {
void speak() {
System.out.println("동물이 소리를 냅니다.");
}
}
class Dog extends Animal {
void speak() {
System.out.println("멍멍!");
}
void wagTail() {
System.out.println("꼬리를 흔들어요!");
}
}
public class Main {
public static void main(String[] args) {
Animal a = new Dog(); // ✅ 부모 타입 변수에 자식 객체 할당
a.speak(); // 출력: 멍멍!
// a.wagTail(); ❌ 에러! Animal 타입에는 wagTail()이 없음
}
}
6.5.3.2. 왜 이렇게 쓰는가? (장점)
- 유연성 확보
- 여러 자식 클래스를 하나의 부모 타입으로 다룰 수 있다.
Animal[] animals = { new Dog(), new Cat(), new Dog() };
for (Animal a : animals) {
a.speak(); // 각 객체에 맞는 speak()가 실행됨
}
- 코드 확장에 유리
- 새로운 자식 클래스가 추가돼도 기존 코드는 수정할 필요 없다.
6.5.3.3. 정리
- 부모 클래스 변수 = new 자식 클래스(); 는 업캐스팅(upcasting) 이라고 한다.
- 이는 자동으로 가능하며, 부모가 가진 기능만 접근 가능.
- 실제 실행 시에는 자식 클래스의 오버라이드된 메서드가 실행됨 → 동적 바인딩 / 다형성
6.5.3.4. 다운캐스팅 (필요 시)
- 부모 타입에서 자식만의 기능을 쓰고 싶다면 다운캐스팅 필요.
Animal a = new Dog();
Dog d = (Dog)a; // 명시적 캐스팅
d.wagTail(); // 이제 자식 메서드도 사용 가능
6.5.4. 다형성 사용 방법
- 다형성을 이용하여 부모 클래스와 자식 클래스를 배열에 넣고 instanceof를 사용하여 타입을 구분하며, 자식 클래스에만 있는 함수에 접근하는 예제 코드
- instanceof을 이용하여 인스턴스를 확인할 경우, 자식 인스턴스에서도 부모 인스턴스 검증을 pass한다. 때문에, 인스턴스 확인을 할 경우, 자식 인스턴스인지 여부를 먼저 확인해야 한다.
class Parent {
void parentMethod() {
System.out.println("Parent method");
}
}
class Child extends Parent {
void childMethod() {
System.out.println("Child method");
}
}
public class Main {
public static void main(String[] args) {
// 부모 클래스와 자식 클래스 객체를 배열에 넣기
Parent[] array = new Parent[2];
array[0] = new Parent();
array[1] = new Child();
// instanceof를 사용하여 타입 구분하고 함수 호출
for (Parent obj : array) {
if (obj instanceof Child) {
Child childObj = (Child) obj; // 형 변환을 통해 자식 클래스의 메서드에 접근 가능
childObj.childMethod(); // 자식 클래스에만 있는 메서드 호출
} else {
obj.parentMethod(); // 부모 클래스의 메서드 호출
}
}
}
}
7. DTO & DAO 정리
7.1. 아키텍처 기본 개념
7.1.1. 계층 구조 예시
Controller → Service → DAO → Database
↑
DTO
- Controller: 요청을 받고 응답을 반환
- Service: 비즈니스 로직 처리
- DAO: DB와의 직접적인 통신
- DTO: 데이터 전송 객체 (계층 간 이동)
7.2. DTO (Data Transfer Object)
7.2.1. 정의
- DTO는 계층 간 데이터 전달을 위한 객체이다. 보통 POJO(Plain Old Java Object)이며, DB Entity와 구조는 유사하지만, 불필요한 로직은 포함되지 않는다.
7.2.2. 특징
- 순수 데이터 전달용 객체
- 로직이 거의 없음 (getter, setter만 있음)
- 직렬화(Serializable) 가능
- API 응답/요청에서 사용됨
public class UserDTO {
private int id;
private String name;
private String email;
public UserDTO() {}
public UserDTO(int id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// Getter/Setter
public int getId() { return id; }
public void setId(int id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
7.3. DAO (Data Access Object)
7.3.1. 정의
- DAO는 DB 작업을 캡슐화한 객체이다. JDBC, JPA, MyBatis 등을 이용해 SQL 쿼리를 수행한다.
7.3.2. 특징
- DB 접근 코드 분리 (유지보수 용이)
- 객체 지향적인 데이터 처리 가능
- 보통 interface + 구현체 구조
7.3.3. 예시 코드 (JDBC 기반)
- 인터페이스
import java.util.List;
public interface UserDAO {
public UserDTO getUserById(int id);
public List<UserDTO> getAllUsers();
public boolean insertUser(UserDTO user);
public boolean updateUser(UserDTO user);
public boolean deleteUser(int id);
}
- 구현체
import java.sql.*;
import java.util.*;
public class UserDAOImpl implements UserDAO {
private Connection conn;
public UserDAOImpl(Connection conn) {
this.conn = conn;
}
@Override
public UserDTO getUserById(int id) {
String sql = "SELECT * FROM users WHERE id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, id);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
return new UserDTO(
rs.getInt("id"),
rs.getString("name"),
rs.getString("email")
);
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
@Override
public List<UserDTO> getAllUsers() {
List<UserDTO> list = new ArrayList<>();
String sql = "SELECT * FROM users";
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
while (rs.next()) {
list.add(new UserDTO(
rs.getInt("id"),
rs.getString("name"),
rs.getString("email")
));
}
} catch (SQLException e) {
e.printStackTrace();
}
return list;
}
@Override
public boolean insertUser(UserDTO user) {
String sql = "INSERT INTO users(name, email) VALUES (?, ?)";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, user.getName());
pstmt.setString(2, user.getEmail());
return pstmt.executeUpdate() == 1;
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean updateUser(UserDTO user) {
String sql = "UPDATE users SET name=?, email=? WHERE id=?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, user.getName());
pstmt.setString(2, user.getEmail());
pstmt.setInt(3, user.getId());
return pstmt.executeUpdate() == 1;
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
@Override
public boolean deleteUser(int id) {
String sql = "DELETE FROM users WHERE id=?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, id);
return pstmt.executeUpdate() == 1;
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
}
7.4. Service 계층과 연동 예
public class UserService {
private UserDAO userDAO;
public UserService(Connection conn) {
this.userDAO = new UserDAOImpl(conn);
}
public void printAllUsers() {
List<UserDTO> users = userDAO.getAllUsers();
for (UserDTO user : users) {
System.out.println(user.getName() + " / " + user.getEmail());
}
}
}
8. java.lang.Object의 equals()와 hashCode() 오버라이딩
- Java 모든 클래스는 java.lang.Object로부터 equals()와 hashCode() 메서드를 상속받는다.
- 기본 구현은 객체의 참조 주소를 비교하도록 되어 있지만, DTO처럼 필드 값 기반으로 동등성을 판단해야 하는 경우 이를 오버라이딩해야 한다
8.1. equals() 오버라이딩의 필요성
8.1.1 기본 동작: 참조 비교
- Object#equals()는 == 연산자와 동일하게 메모리 주소를 비교한다.
8.1.2 내용 기반 비교 요구
- DTO는 주로 네트워크나 계층 간 데이터 전달 역할을 하므로, 필드 값이 같은지에 따라 동등성을 판단해야 한다
8.2. equals() (Contract)
- equals()를 오버라이딩할 때 반드시 아래 5가지 규약을 지켜야 한다:
8.2.1. 반사성(Reflexive): x.equals(x)는 항상 true.
8.2.1.1. 대칭성(Symmetric): x.equals(y)가 true면 y.equals(x)도 true.
8.2.1.2. 추이성(Transitive): x.equals(y)와 y.equals(z)가 true면 x.equals(z)도 true .
8.2.1.3. 일관성(Consistent): 같은 비교를 여러 번 수행해도 결과가 변하지 않음.
8.2.1.4. null 비교: x.equals(null)는 언제나 false.
8.3. hashCode()
- equals()와 짝을 이루는 hashCode() 또한 다음 규약을 따라야 한다.
- 동일 객체: x.equals(y)가 true라면 x.hashCode() == y.hashCode()여야 한다.
- 비교 성능 최적화: 해시코드가 다르면 바로 false를 반환해 성능을 높일 수 있다.
- 일관성: 객체 상태가 변하지 않으면 반복 호출 시 항상 같은 값을 반환해야 한다.
8.4. 오버라이딩 구현 단계
8.4.1. equals() 구현 템플릿
public class MemberDTO {
//중간 생략
public boolean equals(Object obj) {
if (this == obj) return true; // 주소가 같으므로 당연히 true
if (obj == null) return false; // obj가 null이므로 당연히 false
if (getClass() != obj.getClass()) return false; // 클래스의 종류가 다르므로 false
MemberDTO other = (MemberDTO) obj; // 같은 클래스이므로 형변환 실행
//이제부터는 각 인스턴스 변수가 같은지 비교하는 작업 수행
if (name == null) {//name이 null일 때
if (other.name != null) return false;//비교 대상의 name이 null이 아니면 false
} else if (!name.equals(other.name)) return false; //두 개의 email 값이 다르면 false
//name와 같은 비교 수행
if (email == null) {
if (other.email != null) return false;
} else if (!email.equals(other.email)) return false;
if (phone == null) {
if (other.phone != null) return false;
} else if (!phone.equals(other.phone)) return false;
// 모든 난관을 거쳐서 false를 리턴 하지 않은 객체는 같은 값을 가지는 객체로 생각해서 true 를 리턴한다.
return true;
}
}
8.4.2. equals() 메소드를 오버라이딩 할 때에는 반드시 다음 다섯 가지의 조건을 만족시켜야 한다.
- 재귀(reflexive): null이 아닌 x라는 객체의 x.equals(x) 결과는 항상 true여야만 한다.
- 대칭(symmetric): null이 아닌 x와 y 객체가 있을 때 y.equals(x)가 true를 리턴했다면, x.equals(y)도 반드시 true를 리턴해야만 한다.
- 타동적(transitive): null이 아닌 x,y,z가 있을 때 x.equals(y)가 true를 리턴하고, y.equals(z)가 true를 리턴하면 x.equals(z)는 반드시 true를 리턴해야 한다.
- 일관(consistent): null이 아닌 x와 y가 있을 때 객체가 변경되지 않은 상황에서는 몇 번을 호출하더라도 x.equals(y)의 결과는 항상 true이거나 항상 false여야만 한다.
- null과의 비교: null이 아닌 x라는 객체의 x.equals(null) 결과는 항상 false여야만 한다.
8.4.3. hashCode() 구현 템플릿
- hashcode() 메소드는 기본적으로 객체의 메모리 주소를 16진수로 리턴한다.
- 만약 어떤 두 개의 객체가 서로 동일하다면, hashCode() 값은 무조건 동일해야 한다. 따라서 equals() 메소드를 오버라이드 하면, hashCode() 값은 무조건 동일해야 한다.
- 따라서, equals() 메소드를 오버라이드 하면, hashCode() 메소드도 오버라이드 해서 동일한 결과가 나오도록 만들어야 한다.
public class MemberDTO {
//중간 생략
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((email == null) ? 0 : email.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((phone == null) ? 0 : phone.hashCode());
return result;
}
}
8.4.4. hashCode() 메소드를 오버라이딩 할 때에는 반드시 다음 세 가지의 조건을 만족시켜야 한다.
- 자바 애플리케이션이 수행되는 동안에 어떤 객체에 대해서 이 메소드가 호출될 때에는 항상 동일한 int 값을 리턴해 주어야 한다. 하지만, 자바를 실행할 때마다 같은 값이어야 할 필요는 전혀 없다.
- 어떤 두개의 객체에 대하여 equals() 메소드를 사용하여 비교한 결과가 true일 경우에, 두 객체의 hashCode() 메소드를 호출하면 동일한 int 값을 리턴해야만 한다.
- 두 객체를 equals() 메소드를 사용하여 비교한 결과 false를 리턴했다고 해서, hashCode() 메소드를 호출한 int 값이 무조건 달라야 할 필요는 없다. 하지만, 이 경우에 서로 다른 int 값을 제공하면 hashtable의 성능을 향상시키는 데 도움이 된다.
9. 추상화 (abstract)
9.1. 추상화 클래스 생성시 고려사항
- abstract 클래스는 클래스 선언시 abstract이라는 예약어가 클래스 앞에 추가 된다.
- abstract 클래스 안에는 abstract으로 선언된 메소드가 0개 이상 있으면 된다.
- abstract으로 선언된 메소드가 하나라도 있으면, 그 클래스는 반드시 abstract으로 선언되어야만 한다.
- abstract 클래스는 몸통이 있는 메소드가 0개 이상 있어도 전혀 상관 없으며, static이나 final메소드가 있어도 안된다.
(인터페이스는 static이나 final 메소드가 선언되어 있으면 안된다.)
9.2 Java 8/9 이후 추가된 default와 static
9.2.1. Java 8 default 메서드(Default Method):
- Java 8에서 추가된 기능으로, 인터페이스에서 메서드의 구현을 제공할 수 있다.
- 기존 구현된 인터페이스를 변경하지 않고 새로운 기능을 추가할 수 있는 유연성을 제공한다.
9.2.1.1. default 메서드의 목적
- 역호환성 유지: 기존에 구현된 인터페이스에 새로운 기능을 추가할 수 있어 기존 코드와의 호환성을 유지하면서도 새로운 기능을 제공할 수 있다.
- 인터페이스의 기능 확장: 인터페이스가 제공하는 기능을 쉽게 확장할 수 있다. 이는 특히 이미 구현된 많은 클래스들이 사용하는 인터페이스에 유용하다.
- 다중 상속 문제 해결: 다중 상속을 지원하지 않는 Java에서, 여러 인터페이스로부터의 기본 구현을 제공하는 데 유용하다.
9.2.1.2. default 메서드의 사용 방법
- default 메서드는 메서드 선언 앞에 default 키워드를 붙이고, 본문을 구현한다.
- 인터페이스를 구현하는 클래스에서 이 메서드를 재정의할 수 있지만, 재정의는 선택 사항이다.
- 인터페이스 내부에서만 구현할 수 있으며, private, protected, public 등의 접근 제어자는 사용할 수 없다.
9.2.1.3. 사용 예시
public interface FileSystem {
void readFile(String path);
void writeFile(String path, String content);
default void deleteFile(String path) {
// 기본 구현: 파일 삭제 로직
System.out.println("Deleting file: " + path);
}
}
public interface ExampleInterface {
default void method1() {
// 기본 구현
System.out.println("Default implementation of method1");
}
default void method2() {
// 기본 구현
System.out.println("Default implementation of method2");
}
}
9.2.2. Java 8 static 메서드(Static Method):
- 마찬가지로 Java 8에서 추가된 기능으로, 인터페이스에 정적 메서드를 정의할 수 있다.
- 인터페이스 이름을 통해 직접 호출할 수 있다.
public interface ExampleInterface {
static void staticMethod() {
System.out.println("Static method in interface");
}
}
9.2.3. Java 9 static 변수(Static Variables)
- 인터페이스에서 private static 변수를 선언하여 상수를 정의할 수 있다.
- 이는 인터페이스 내부에서만 접근 가능하며, 구현 클래스에서 사용할 수 없다.
public interface ExampleInterface {
private static final int MAX_VALUE = 100;
static void printMaxValue() {
System.out.println("Max value: " + MAX_VALUE);
}
}
10. final
10.1. final 변수
- 한 번만 값을 할당할 수 있는 변수이다.
- 주로 상수 정의에 사용되며, 값이 변경되면 안 되는 경우에 사용한다.
final int number = 10;
number = 20; // 컴파일 에러! 한 번 초기화한 값은 변경 불가
- 객체를 참조하는 final 변수는 참조는 못 바꾸지만 객체 내부의 필드는 변경 가능하다.
final List<String> list = new ArrayList<>();
list.add("hello"); // OK
list = new ArrayList<>(); // 에러! 참조 변경 불가
10.2. final 메서드
- 오버라이딩(재정의)할 수 없는 메서드이다.
- 보통 상속받은 클래스가 변경하면 안 되는 핵심 동작에 사용된다.
class Parent {
final void printMessage() {
System.out.println("Hello from parent");
}
}
class Child extends Parent {
// void printMessage() {} // 컴파일 에러! 오버라이딩 불가
}
10.3. final 클래스
- 상속할 수 없는 클래스이다.
- 다른 클래스가 확장하지 못하게 막고 싶을 때 사용한다.
final class Animal {
// ...
}
// class Dog extends Animal {} // 에러! 상속 불가
10.4. 요약
대상 | 역할 |
변수 | 값 변경 금지 (한 번만 할당 가능) |
메서드 | 오버라이딩 금지 |
클래스 | 상속 금지 |
10.5. final 지역 변수 / 매개변수의 초기화 규칙
구분 | 초기화 필요 여부 | 초기화 시점 | 재할당 가능 여부 |
final 지역 변수 | ❌ 선언 시 즉시 초기화는 선택 | 사용 전에 반드시 한 번만 초기화 | ❌ 불가능 |
final 매개변수 | ✅ 메서드 호출 시 전달됨 | 메서드 호출 시 값이 고정됨 (재할당 불가) | ❌ 불가능 |
10.5.1. final 지역 변수
- 선언만 먼저 하고, 나중에 값을 지정하는 건 OK
- 하지만 두 번 이상 할당하면 컴파일 에러
public class Example {
public static void main(String[] args) {
final int number; // 초기화 안 해도 컴파일 OK
number = 10; // 최초 1회만 가능
System.out.println(number);
// number = 20; // ❌ 컴파일 에러! 이미 값 할당됨
}
}
10.5.2. final 지역 변수
- 모든 코드 경로에서 반드시 한 번만 초기화되어야 한다.
- 하나의 경로에서라도 빠뜨리면 컴파일 에러 발생한다.
public class Example {
public static void main(String[] args) {
final int number;
if (args.length > 0) {
number = 1;
} else {
number = 2;
}
System.out.println(number); // OK: 모든 경로에서 초기화됨
}
}
10.5.3. final 매개변수
- final 매개변수는 함수 호출 시 값이 들어오고, 그 이후 바꿀 수 없다.
public class Example {
public static void printMessage(final String msg) {
// msg = "Hello"; // ❌ 컴파일 에러! final 매개변수 재할당 불가
System.out.println(msg);
}
public static void main(String[] args) {
printMessage("Hi!");
}
}
10.5.4. 언제 사용할까?
사용처 | 이유 |
지역 변수에 final | 실수로 값 바꾸는 것 방지, 안정성 증가 |
매개변수에 final | 전달받은 값을 그대로 쓰겠다는 의도 전달 |
람다식, 익명 클래스 | 지역 변수는 final이거나 사실상 final이어야 참조 가능 |
11. enum 열거형 (enumeration)
11.1. 기본 개념
- enum은 열거형(enumeration)의 줄임말로, 한정된 상수 집합을 정의할 때 사용.
- Java의 enum은 사실상 클래스이며, 내부에 필드, 생성자, 메소드 정의 가능.
11.2. enum 사용 방식
11.2.1. 단순 타입(enum 상수만 사용)
public enum Direction {
NORTH, SOUTH, EAST, WEST
}
Direction d = Direction.NORTH;
11.2.2. 상수에 값을 부여
public enum Grade {
BASIC(1),
SILVER(2),
GOLD(3);
private final int level;
Grade(int level) {
this.level = level;
}
public int getLevel() {
return level;
}
}
int goldLevel = Grade.GOLD.getLevel(); // 3
11.3. enum에서 오버라이딩 불가능한 Object 메소드
- 이 메소드들은 enum이 Java 컴파일러에 의해 자동 생성되는 내부 구현에서 이미 final로 선언되어 있기 때문에 오버라이드할 수 없다.
메소드 | 오버라이드 가능 여부 | 설명 |
clone() | ❌ 불가능 | enum은 자동으로 final이고, Cloneable을 구현하지 않으며, 복제를 막음 |
finalize() | ❌ 불가능 | enum에서는 오버라이딩 자체가 금지됨 |
equals(Object) | ❌ 불가능 | enum은 == 비교만 허용되며, equals()는 오버라이딩이 금지됨 |
hashCode() | ❌ 불가능 | 오버라이딩 금지. 컴파일 오류 발생 |
11.4. enum에서 오버라이딩 가능한 메소드
메소드 | 오버라이드 가능 여부 | 설명 |
toString() | ✅ 가능 | 사용자 정의 출력 문자열 |
11.5. enum 클래스 전용 메서드 정리 (Object 제외)
메서드 이름 | 리턴 타입 | 설명 | 컴파일러 자동 생성 여부 |
values() | EnumType[] | enum의 모든 상수를 배열로 반환 | ✅ (컴파일러가 자동 생성) |
valueOf(String name) | EnumType | 주어진 이름과 일치하는 enum 상수 반환 | ✅ (Enum.valueOf() 기반으로 생성됨) |
ordinal() | int | enum 상수의 선언 순서 (0부터 시작) 반환 | ❌ (Enum 클래스에 정의됨, final) |
name() | String | enum 상수 이름 반환 | ❌ (Enum 클래스에 정의됨, final) |
getDeclaringClass() | Class<E> | enum 타입 클래스 반환 | ❌ (Enum 클래스에 정의됨) |
compareTo(E other) | int | 선언 순서를 기준으로 비교 | ❌ (Comparable 인터페이스 구현) |
11.6. 사용 예시
public enum Product {
APPLE(1000),
BANANA(500),
ORANGE(800);
private final int price; // 상수마다 고정된 값
private int amount; // 변경 가능한 상태 값
Product(int price) {
this.price = price;
this.amount = 0; // 초기값 설정
}
public int getPrice() {
return price;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
}
public class Main {
public static void main(String[] args) {
Product apple = Product.APPLE;
apple.setAmount(10);
System.out.println("상품: " + apple.name());
System.out.println("가격: " + apple.getPrice());
System.out.println("수량: " + apple.getAmount());
}
}
12. 예외처리 try - catch - final
12.1. Java 예외의 기본 개념
12.1.1. 예외(Exception)란?
- 프로그램 실행 중 발생할 수 있는 비정상적인 상황(에러)을 객체로 표현한 것. 자바는 예외가 발생하면 Exception 객체를 생성하고 이를 JVM이 처리한다.
12.2. 예외의 종류
분류 | 설명 |
Checked Exception | 컴파일 시 체크되는 예외. 반드시 try-catch 또는 throws로 처리해야 함. |
예: IOException, SQLException | |
Unchecked Exception | 런타임 시 발생. 컴파일러가 강제하지 않음. |
예: NullPointerException, IllegalArgumentException | |
Error | 시스템 레벨의 심각한 문제. 개발자가 처리하지 않음. |
예: OutOfMemoryError, StackOverflowError |
12.3. try-catch 문과 여러 개의 catch 사용
try {
// 예외 발생 가능 코드
} catch (IOException e) {
// IO 관련 예외 처리
} catch (SQLException e) {
// DB 관련 예외 처리
} catch (Exception e) {
// 나머지 예외 처리 (항상 마지막에 위치)
}
12.3.1. 주의할 점
- 부모 타입의 catch 블록은 항상 하단에 있어야 함.
- 순서가 잘못되면 컴파일 에러 발생.
12.4. try-catch-finally
- Java에서는 예외가 발생하든 그렇지 않든 반드시 실행되어야 하는 코드가 있을 때 finally 블록을 사용한다.
try {
// 예외 발생 가능 코드
} catch (ExceptionType e) {
// 예외 처리 코드
} finally {
// 항상 실행됨 (예외 발생 여부와 무관)
}
12.5. try-with-resources 문법 개요
try (ResourceType resource = new ResourceType()) {
// 자원을 사용하는 코드
} catch (Exception e) {
// 예외 처리
}
// try 블록이 끝나면 resource는 자동으로 close() 됨
- () 안에 선언된 자원은 try 블록이 끝날 때 자동으로 close() 메서드가 호출된다.
- 해당 자원은 AutoCloseable 또는 Closeable 인터페이스를 구현해야 한다.
- 다른 선언 방법
try (BufferedReader br = new BufferedReader(new FileReader("sample.txt"))) {
// 한 줄만 선언해도 무방
}
try (
리소스1;
리소스2
) {
// 코드
}
try (
FileReader fr = new FileReader(filePath); // OK
BufferedReader br = new BufferedReader(fr); // ❌ 마지막에 세미콜론 ❌
) {
// ...
}
12.5.1. 예제 코드: 파일에서 한 줄씩 읽기
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class FileReadExample {
public static void main(String[] args) {
// 읽을 파일 경로 지정
String filePath = "sample.txt";
// try-with-resources 사용: FileReader와 BufferedReader 모두 AutoCloseable을 구현함
try (
FileReader fr = new FileReader(filePath); // 파일을 읽기 위한 FileReader
BufferedReader br = new BufferedReader(fr) // 버퍼를 통한 효율적인 읽기
) {
String line;
// 한 줄씩 읽기
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
// 파일이 없거나 읽기 중 오류 발생 시 처리
System.err.println("파일을 읽는 도중 오류가 발생했습니다: " + e.getMessage());
}
// fr과 br은 try 블록을 벗어나면 자동으로 close() 됨
}
}
12.5.2. 추가 예: 사용자 정의 자원 클래스
class MyResource implements AutoCloseable {
public void doSomething() {
System.out.println("자원 사용 중...");
}
@Override
public void close() {
System.out.println("자원 해제 완료 (close() 호출됨)");
}
}
public class CustomResourceExample {
public static void main(String[] args) {
try (MyResource resource = new MyResource()) {
resource.doSomething();
} // 여기서 자동으로 close() 호출됨
}
}
12.6. throws 키워드
- 메서드에서 발생할 수 있는 예외를 호출한 쪽으로 전가함.
public void readFile(String path) throws IOException {
FileReader reader = new FileReader(path);
}
12.7. try-catch vs throws
try-catch | throws |
예외를 직접 처리 | 예외를 전달 |
코드 복잡도가 증가 | 호출자에게 책임을 넘김 |
로직에 따라 다양한 예외 처리 가능 | 프레임워크/라이브러리에서 깔끔한 인터페이스 제공 시 유리 |
12.8. Throwable 클래스
public class Throwable {
public Throwable();
public Throwable(String message);
public Throwable(String message, Throwable cause);
public Throwable(Throwable cause);
}
12.9. 자주 오버라이딩되는 Throwable 메서드
12.9.1. getMessage()
- 예외의 메시지를 반환 (생성자에 전달된 문자열)
e.getMessage(); // "파일이 존재하지 않습니다"
12.9.1. toString()
- 예외의 클래스명 + 메시지 출력
System.out.println(e.toString());
// java.io.FileNotFoundException: 파일이 존재하지 않습니다
12.9.1. printStackTrace()
- 예외 발생 당시의 호출 스택을 출력
e.printStackTrace();
12.10. 커스텀 예외(Custom Exception)
public class MyCustomException extends Exception {
public MyCustomException(String message) {
super(message);
}
}
public void doSomething(int value) throws MyCustomException {
if (value < 0) {
throw new MyCustomException("음수는 처리할 수 없습니다.");
}
}
public class BusinessException extends RuntimeException {
private final String errorCode;
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
// 사용 예
if (!user.isActive()) {
throw new BusinessException("USER_INACTIVE", "사용자 계정이 비활성화 상태입니다.");
}
12.11. 실무 기반 예외 처리 시나리오 10선
시나리오 | 설명 및 예외 전략 | 코드 예시 |
1. DB 연결 실패 | 로그 기록 후 사용자에게 일반 메시지 제공 | SQLException → CustomException 변환 |
2. 잘못된 사용자 입력 | 즉시 검증 후 IllegalArgumentException 발생 | form validation |
3. REST API 호출 실패 | HTTP 상태 코드로 변환 후 클라이언트 전달 | HttpClientException |
4. 인증 실패 | AuthenticationException → 401 UNAUTHORIZED | Spring Security |
5. 파일 업로드 실패 | IOException → 사용자 메시지 전환 | Multipart 처리 시 try-catch |
6. 예약어 중복 입력 | 비즈니스 로직 예외 DuplicateKeywordException | CustomException 활용 |
7. 타임아웃 | SocketTimeoutException catch 후 재시도 로직 | 외부 API 연동 |
8. 정합성 오류 | InvalidStateException 발생 | 도메인 상태 체크 |
9. Enum 매핑 실패 | IllegalArgumentException → EnumTypeException | valueOf 시 주의 |
10. 시스템 장애 발생 | 모든 예외 로깅 + 슬랙 알림 전송 | @ControllerAdvice + AOP 사용 |
13. 스트링 String
13.1. String 생성자
생성자 서명 | 설명 |
String() | 빈 문자열을 생성합니다 Oracle Documentation. |
String(byte[] bytes) | 플랫폼 기본 charset으로 바이트 배열을 디코딩해 문자열을 생성합니다 Oracle Documentation. |
String(byte[] bytes, Charset charset) | 지정된 Charset으로 바이트 배열을 디코딩해 문자열을 생성합니다 Oracle Documentation. |
String(byte[] bytes, int offset, int length) | 지정된 부분 배열을 플랫폼 기본 charset으로 디코딩해 문자열을 생성합니다 Oracle Documentation. |
String(byte[] bytes, int offset, int length, Charset charset) | 지정된 부분 배열을 주어진 Charset으로 디코딩해 문자열을 생성합니다 Oracle Documentation. |
String(byte[] bytes, int offset, int length, String charsetName) | 지정된 부분 배열을 주어진 charset 이름으로 디코딩해 문자열을 생성합니다 Oracle Documentation. |
String(byte[] bytes, String charsetName) | 주어진 charset 이름으로 바이트 배열을 디코딩해 문자열을 생성합니다 Oracle Documentation. |
String(char[] value) | 문자 배열 전체를 복사해 문자열을 생성합니다 Oracle Documentation. |
String(char[] value, int offset, int count) | 문자 배열의 일부분을 복사해 문자열을 생성합니다 Oracle Documentation. |
String(int[] codePoints, int offset, int count) | 유니코드 코드포인트 배열의 일부분을 복사해 문자열을 생성합니다 Oracle Documentation. |
String(String original) | 주어진 문자열의 복사본을 생성합니다 Oracle Documentation. |
String(StringBuffer buffer) | StringBuffer의 내용을 복사해 문자열을 생성합니다 Oracle Documentation. |
String(StringBuilder builder) | StringBuilder의 내용을 복사해 문자열을 생성합니다 Oracle Documentation. |
13.2. 주요 생성자 상세
13.2.1. String(byte[] bytes)
- 역할: 플랫폼의 기본 charset으로 바이트 배열을 디코딩하여 문자열을 생성한다.
- 특징:
- 생성된 문자열의 길이는 charset에 따라 달라질 수 있으며, 바이트 배열 길이와 같지 않을 수 있다.
- 유효하지 않은 바이트 시퀀스가 포함된 경우 동작이 명시되지 않으므로, 세부 제어가 필요하면 CharsetDecoder 사용을 권장한다.
byte[] data = {72, 101, 108, 108, 111};
String s = new String(data); // "Hello"
13.2.2. String(byte[] bytes, String charsetName)
- 역할: 지정된 charset 이름으로 바이트 배열을 디코딩하여 문자열을 생성한다.
- 특징:
- UnsupportedEncodingException을 던질 수 있다.
- 동작 방식과 주의사항은 기본 생성자와 동일하며, charset 지정만 다르다.
byte[] utf8Data = {(byte)0xE2, (byte)0x98, (byte)0x83};
String s = new String(utf8Data, "UTF-8"); // "☃"
13.3. 문자열 ↔ 바이트 변환
13.3.1. getBytes 메소드 정리
메소드 | 매개변수 | 반환 타입 | 설명 |
byte[] getBytes() | 없음 | byte[] | 플랫폼 기본 문자집합으로 인코딩하여 바이트 배열 생성 Oracle Docs |
byte[] getBytes(Charset charset) | Charset charset | byte[] | 지정된 Charset으로 인코딩하여 바이트 배열 생성 Oracle Docs |
byte[] getBytes(String charset) | String charsetName | byte[] | 지정된 인코딩 이름으로 인코딩하여 바이트 배열 생성 Oracle Docs |
13.3.2. 주요 문자집합(UCS) 정리
인코딩 이름 | 설명 |
UTF-16 | Java 내부 기본 인코딩. 원래 UCS‑2 기반으로 시작하여 UTF‑16 지원 |
UTF-16BE/LE | 빅/리틀 엔디언 UTF‑16 |
UTF-8 | 가변 길이 유니코드. 네트워크·파일 I/O 표준 인코딩 |
ISO-8859-1 | Western Europe 1바이트 인코딩 |
US-ASCII | 7비트 ASCII 인코딩 |
13.3.3. 예시 코드
// 기본 charset을 사용해 문자열을 바이트 배열로 변환하고 출력 후 다시 String 생성
public class DefaultCharsetExample {
public static void main(String[] args) throws Exception {
String original = "안녕하세요";
byte[] bytes = original.getBytes(); // 플랫폼 기본 charset 인코딩
for (byte b : bytes) {
System.out.printf("%02X ", b); // 각 바이트 16진수 출력
}
System.out.println();
String decoded = new String(bytes); // 바이트 배열로부터 String 생성
System.out.println(decoded);
}
}
// UTF-16을 사용해 문자열을 바이트 배열로 변환하고 처리하는 시나리오
public class Utf16Example {
public static void main(String[] args) throws Exception {
String original = "Hello, 世界";
byte[] utf16Bytes = original.getBytes("UTF-16"); // UTF-16 인코딩
for (int i = 0; i < utf16Bytes.length; i += 2) {
char c = (char) ((utf16Bytes[i] << 8) | (utf16Bytes[i+1] & 0xFF));
System.out.print(c);
}
System.out.println();
String decoded = new String(utf16Bytes, "UTF-16"); // UTF-16 디코딩
System.out.println(decoded);
}
}
13.4. 문자열 비교 및 검색 메소드
13.4.1. 길이 및 빈 문자열 확인
메소드 | 반환 타입 | 설명 | 예시 |
length() | int | 문자열의 길이(문자 수)를 반환합니다 | str.length(); |
isEmpty() | boolean | 길이가 0이면 true, 아니면 false를 반환합니다 | str.isEmpty(); |
13.4.2. 문자열 내용을 비교하고 검색하는 메소드들
13.4.2.1. 문자열의 길이를 확인하는 메소드
메소드 이름 | 리턴 타입 | 매개변수 | 설명 |
length() | int | 없음 | ㅖ |
- 예시 코드
public class StringLengthExample {
public static void main(String[] args) {
String str = "Hello, World!";
// 문자열의 길이를 확인하는 예시
int length = str.length();
System.out.println("문자열 \"" + str + "\"의 길이는 " + length + "입니다.");
}
}
문자열 "Hello, World!"의 길이는 13입니다.
13.4.2.2. 문자열이 비어 있는지 확인하는 메소드
리턴 타입 | 메소드 이름 | 매개 변수 | 설명 |
boolean | isEmpty() | 없음 | 문자열이 비어 있는지 여부를 확인하여 결과를 true 혹은 false로 반환합니다. |
- 예시 코드
public class StringIsEmptyExample {
public static void main(String[] args) {
String str1 = ""; // 빈 문자열
String str2 = "Hello, World!"; // 비어 있지 않은 문자열
// isEmpty() 메소드를 사용하여 문자열이 비어 있는지 확인
boolean isEmpty1 = str1.isEmpty();
boolean isEmpty2 = str2.isEmpty();
// 결과 출력
System.out.println("str1이 비어 있는지 확인: " + isEmpty1);
System.out.println("str2가 비어 있는지 확인: " + isEmpty2);
}
}
str1이 비어 있는지 확인: true
str2가 비어 있는지 확인: false
13.4.2.3. 문자열이 같은지 비교하는 메소드
리턴 타입 | 메소드 이름 및 매개 변수 | 설명 |
boolean | equals(Object obj) | 주어진 객체(obj)와 문자열이 동일한지 비교합니다. |
boolean | equalsIgnoreCase(String anotherString) | 대소문자를 무시하고 주어진 문자열과 동일한지 비교합니다. |
boolean | contentEquals(CharSequence cs) | 지정된 문자 시퀀스(cs)와 문자열이 동일한지 비교합니다. |
boolean | contentEquals(StringBuffer sb) | 지정된 StringBuffer(sb)와 문자열이 동일한지 비교합니다. |
boolean | startsWith(String prefix) | 주어진 접두사(prefix)로 시작하는지 여부를 확인합니다. |
boolean | endsWith(String suffix) | 주어진 접미사(suffix)로 끝나는지 여부를 확인합니다. |
int | compareTo(String anotherString) | 사전 순서로 문자열을 비교하여 비교 결과를 반환합니다. |
int | compareToIgnoreCase(String str) | 대소문자를 무시하고 사전 순서로 문자열을 비교하여 비교 결과를 반환합니다. |
- 예시 코드
public class StringComparisonExample {
public static void main(String[] args) {
String str1 = "Hello";
String str2 = "hello";
String str3 = "Hello";
String str4 = "World";
// equals(Object obj)
boolean isEqual1 = str1.equals(str3);
System.out.println("str1.equals(str3): " + isEqual1); // true
// equalsIgnoreCase(String anotherString)
boolean isEqual2 = str1.equalsIgnoreCase(str2);
System.out.println("str1.equalsIgnoreCase(str2): " + isEqual2); // true
// contentEquals(CharSequence cs)
StringBuilder sb = new StringBuilder("Hello");
boolean isEqual3 = str1.contentEquals(sb);
System.out.println("str1.contentEquals(sb): " + isEqual3); // true
// contentEquals(StringBuffer sb)
StringBuffer stringBuffer = new StringBuffer("Hello");
boolean isEqual4 = str1.contentEquals(stringBuffer);
System.out.println("str1.contentEquals(stringBuffer): " + isEqual4); // true
// startsWith(String prefix)
boolean startsWith = str1.startsWith("He");
System.out.println("str1.startsWith(\"He\"): " + startsWith); // true
// endsWith(String suffix)
boolean endsWith = str1.endsWith("lo");
System.out.println("str1.endsWith(\"lo\"): " + endsWith); // true
// compareTo(String anotherString)
int compareResult = str1.compareTo(str4);
System.out.println("str1.compareTo(str4): " + compareResult); // negative value
// compareToIgnoreCase(String str)
int compareIgnoreCaseResult = str1.compareToIgnoreCase(str2);
System.out.println("str1.compareToIgnoreCase(str2): " + compareIgnoreCaseResult); // 0 (equal)
}
}
str1.equals(str3): true
str1.equalsIgnoreCase(str2): true
str1.contentEquals(sb): true
str1.contentEquals(stringBuffer): true
str1.startsWith("He"): true
str1.endsWith("lo"): true
str1.compareTo(str4): negative value
str1.compareToIgnoreCase(str2): 0
13.4.2.4. 특정 조건에 맞는 문자열이 있는지를 확인하는 메소드
리턴 타입 | 메소드 이름 및 매개 변수 | 설명 |
boolean | contains(CharSequence sequence) | 주어진 문자 시퀀스(sequence)가 문자열에 포함되어 있는지 확인합니다. |
boolean | matches(String regex) | 정규 표현식(regex)과 문자열이 매칭되는지 확인합니다. |
boolean | startsWith(String prefix) | 주어진 접두사(prefix)로 시작하는지 여부를 확인합니다. |
boolean | startsWith(String prefix, int toffset) | 지정된 위치(toffset)에서 주어진 접두사(prefix)로 시작하는지 여부를 확인합니다. |
boolean | endsWith(String suffix) | 주어진 접미사(suffix)로 끝나는지 여부를 확인합니다. |
boolean | regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len) | 주어진 위치(toffset)에서 다른 문자열(other)의 일부(len)가 동일한지 여부를 확인합니다. 대소문자 무시 여부는 ignoreCase에 따라 결정됩니다. |
boolean | regionMatches(int toffset, String other, int ooffset, int len) | 주어진 위치(toffset)에서 다른 문자열(other)의 일부(len)가 동일한지 여부를 확인합니다. |
public class StringContainsExample {
public static void main(String[] args) {
String str = "Hello, World!";
// contains(CharSequence sequence)
boolean contains1 = str.contains("World");
System.out.println("str.contains(\"World\"): " + contains1); // true
// matches(String regex)
boolean matches = str.matches(".*World.*");
System.out.println("str.matches(\".*World.*\"): " + matches); // true
// startsWith(String prefix)
boolean startsWith1 = str.startsWith("Hello");
System.out.println("str.startsWith(\"Hello\"): " + startsWith1); // true
// startsWith(String prefix, int toffset)
boolean startsWith2 = str.startsWith("World", 7);
System.out.println("str.startsWith(\"World\", 7): " + startsWith2); // true
// endsWith(String suffix)
boolean endsWith = str.endsWith("!");
System.out.println("str.endsWith(\"!\"): " + endsWith); // true
// regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len)
boolean regionMatches1 = str.regionMatches(true, 7, "world", 0, 5);
System.out.println("str.regionMatches(true, 7, \"world\", 0, 5): " + regionMatches1); // true
// regionMatches(int toffset, String other, int ooffset, int len)
boolean regionMatches2 = str.regionMatches(7, "World", 0, 5);
System.out.println("str.regionMatches(7, \"World\", 0, 5): " + regionMatches2); // true
}
}
str.contains("World"): true
str.matches(".*World.*"): true
str.startsWith("Hello"): true
str.startsWith("World", 7): true
str.endsWith("!"): true
str.regionMatches(true, 7, "world", 0, 5): true
str.regionMatches(7, "World", 0, 5): true
13.4.2.5. regionMatches의 매개 변수와 사용법 이해
- regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len)
- 이 메소드는 문자열에서 대소문자를 구분 여부를 선택할 수 있으며, 비교할 위치에서 시작하여 다른 문자열의 일부를 비교한다.
- 동작:
- 주어진 toffset에서 시작하여, other 문자열의 ooffset 위치에서부터 len 길이만큼의 문자열을 비교한다.
- ignoreCase가 true일 경우, 비교할 때 대소문자를 구분하지 않는다.
- 비교 결과는 해당 영역이 동일하면 true, 그렇지 않으면 false를 반환한다.
- regionMatches(int toffset, String other, int ooffset, int len)
- 이 메소드는 문자열에서 대소문자를 구분하여 지정된 위치에서부터 다른 문자열의 일부를 비교한다.
- 동작:
- 주어진 toffset에서 시작하여, other 문자열의 ooffset 위치에서부터 len 길이만큼의 문자열을 비교한다.
- 대소문자를 구분하여 비교한다.
- 비교 결과는 해당 영역이 동일하면 true, 그렇지 않으면 false를 반환한다.
매개변수 | 의미 |
ignoreCase | 대소문자를 무시할지 여부를 결정하는 플래그입니다. true면 대소문자를 구분하지 않습니다. |
toffset | 문자열에서 비교를 시작할 위치입니다. |
other | 비교할 다른 문자열입니다. |
ooffset | 비교할 다른 문자열에서 비교를 시작할 위치입니다. |
len | 비교할 문자의 길이입니다. |
public class StringRegionMatchesExample {
public static void main(String[] args) {
String str = "Hello, World!";
// Using regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len)
boolean matchIgnoreCase = str.regionMatches(true, 7, "world", 0, 5);
System.out.println("str.regionMatches(true, 7, \"world\", 0, 5): " + matchIgnoreCase); // true
boolean matchCaseSensitive = str.regionMatches(false, 7, "World", 0, 5);
System.out.println("str.regionMatches(false, 7, \"World\", 0, 5): " + matchCaseSensitive); // false
// Using regionMatches(int toffset, String other, int ooffset, int len)
boolean match = str.regionMatches(7, "World", 0, 5);
System.out.println("str.regionMatches(7, \"World\", 0, 5): " + match); // true
}
}
str.regionMatches(true, 7, "world", 0, 5): true
str.regionMatches(false, 7, "World", 0, 5): false
str.regionMatches(7, "World", 0, 5): true
13.5. 문자열 내에서 위치를 찾아내는 방법
13.5.1. 위치를 찾는 메소드
리턴 타입 | 메소드 이름 및 매개 변수 | 설명 |
int | indexOf(int ch) | 주어진 문자(Unicode 코드 포인트)의 인덱스를 반환합니다. 문자가 없으면 -1을 반환합니다. |
int | indexOf(int ch, int fromIndex) | 주어진 인덱스부터 시작하여 주어진 문자의 인덱스를 반환합니다. |
int | indexOf(String str) | 주어진 문자열의 첫 번째 출현 위치의 인덱스를 반환합니다. 문자열이 없으면 -1을 반환합니다. |
int | indexOf(String str, int fromIndex) | 주어진 인덱스부터 시작하여 주어진 문자열의 인덱스를 반환합니다. |
int | lastIndexOf(int ch) | 주어진 문자(Unicode 코드 포인트)의 마지막 출현 위치의 인덱스를 반환합니다. 문자가 없으면 -1을 반환합니다. |
int | lastIndexOf(int ch, int fromIndex) | 주어진 인덱스부터 시작하여 주어진 문자의 마지막 출현 위치의 인덱스를 반환합니다. |
int | lastIndexOf(String str) | 주어진 문자열의 마지막 출현 위치의 인덱스를 반환합니다. 문자열이 없으면 -1을 반환합니다. |
int | lastIndexOf(String str, int fromIndex) | 주어진 인덱스부터 시작하여 주어진 문자열의 마지막 출현 위치의 인덱스를 반환합니다. |
public class StringIndexOfExample {
public static void main(String[] args) {
String str = "Hello, World!";
// indexOf(int ch)
int index1 = str.indexOf('o');
System.out.println("str.indexOf('o'): " + index1); // 4
// indexOf(int ch, int fromIndex)
int index2 = str.indexOf('o', 5);
System.out.println("str.indexOf('o', 5): " + index2); // 8
// indexOf(String str)
int index3 = str.indexOf("World");
System.out.println("str.indexOf(\"World\"): " + index3); // 7
// indexOf(String str, int fromIndex)
int index4 = str.indexOf("o", 5);
System.out.println("str.indexOf(\"o\", 5): " + index4); // 8
// lastIndexOf(int ch)
int lastIndex1 = str.lastIndexOf('o');
System.out.println("str.lastIndexOf('o'): " + lastIndex1); // 8
// lastIndexOf(int ch, int fromIndex)
int lastIndex2 = str.lastIndexOf('o', 7);
System.out.println("str.lastIndexOf('o', 7): " + lastIndex2); // 4
// lastIndexOf(String str)
int lastIndex3 = str.lastIndexOf("o");
System.out.println("str.lastIndexOf(\"o\"): " + lastIndex3); // 8
// lastIndexOf(String str, int fromIndex)
int lastIndex4 = str.lastIndexOf("o", 7);
System.out.println("str.lastIndexOf(\"o\", 7): " + lastIndex4); // 4
}
}
str.indexOf('o'): 4
str.indexOf('o', 5): 8
str.indexOf("World"): 7
str.indexOf("o", 5): 8
str.lastIndexOf('o'): 8
str.lastIndexOf('o', 7): 4
str.lastIndexOf("o"): 8
str.lastIndexOf("o", 7): 4
13.6. 문자열의 값의 일부를 추출하기 위한 메소드
13.6.1. char 단위의 값을 추출하는 메소드
리턴 타입 | 메소드 이름 및 매개 변수 | 설명 |
char | charAt(int index) | 주어진 인덱스 위치의 문자를 반환합니다. |
void | getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) | 문자열의 지정된 부분을 문자 배열에 복사합니다. |
int | codePointAt(int index) | 주어진 인덱스 위치의 코드 포인트 값을 반환합니다. |
int | codePointBefore(int index) | 주어진 인덱스 바로 이전 위치의 코드 포인트 값을 반환합니다. |
int | codePointCount(int beginIndex, int endIndex) | 주어진 범위 내의 코드 포인트 수를 반환합니다. |
int | offsetByCodePoints(int index, int codePointOffset) | 주어진 인덱스에서부터 코드 포인트 오프셋만큼 이동한 인덱스를 반환합니다. |
public class StringCharMethodsExample {
public static void main(String[] args) {
String str = "Hello, World!";
char[] dest = new char[6];
// charAt(int index)
char ch1 = str.charAt(4);
System.out.println("str.charAt(4): " + ch1); // 'o'
// getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)
str.getChars(7, 12, dest, 0);
System.out.print("str.getChars(7, 12, dest, 0): ");
System.out.println(dest); // "World"
// codePointAt(int index)
int codePoint1 = str.codePointAt(1);
System.out.println("str.codePointAt(1): " + codePoint1); // 101 ('e')
// codePointBefore(int index)
int codePoint2 = str.codePointBefore(8);
System.out.println("str.codePointBefore(8): " + codePoint2); // 87 ('W')
// codePointCount(int beginIndex, int endIndex)
int codePointCount = str.codePointCount(0, 5);
System.out.println("str.codePointCount(0, 5): " + codePointCount); // 5
// offsetByCodePoints(int index, int codePointOffset)
int offsetIndex = str.offsetByCodePoints(0, 5);
System.out.println("str.offsetByCodePoints(0, 5): " + offsetIndex); // 5
}
}
str.charAt(4): o
str.getChars(7, 12, dest, 0): World
str.codePointAt(1): 101
str.codePointBefore(8): 87
str.codePointCount(0, 5): 5
str.offsetByCodePoints(0, 5): 5
13.6.2. char 배열의 값을 string으로 변환하는 메소드
리턴 타입 | 메소드 이름 및 매개 변수 | 설명 |
String | copyValueOf(char[] data) | 주어진 char 배열의 모든 요소를 포함하는 문자열을 생성합니다. |
String | copyValueOf(char[] data, int offset, int count) | 주어진 char 배열의 지정된 범위의 요소를 포함하는 문자열을 생성합니다. |
public class CopyValueOfExample {
public static void main(String[] args) {
// char 배열 선언
char[] charArray = {'H', 'e', 'l', 'l', 'o'};
// copyValueOf(char[] data) 메소드 사용
String str1 = String.copyValueOf(charArray);
System.out.println("String str1 = String.copyValueOf(charArray): " + str1); // "Hello"
// copyValueOf(char[] data, int offset, int count) 메소드 사용
String str2 = String.copyValueOf(charArray, 1, 3);
System.out.println("String str2 = String.copyValueOf(charArray, 1, 3): " + str2); // "ell"
}
}
String str1 = String.copyValueOf(charArray): Hello
String str2 = String.copyValueOf(charArray, 1, 3): ell
13.6.3. 문자열 값을 char 배열로 변환하는 메소드
리턴 타입 | 메소드 이름 및 매개 변수 | 설명 |
char[] | toCharArray() | 문자열을 char 배열로 변환하여 반환합니다. |
public class ToCharArrayExample {
public static void main(String[] args) {
// 문자열 선언
String str = "Hello";
// toCharArray() 메소드 사용
char[] charArray = str.toCharArray();
// char 배열 출력
System.out.print("charArray: ");
for (char ch : charArray) {
System.out.print(ch + " ");
}
System.out.println();
}
}
charArray: H e l l o
13.6.4. 문자열의 일부 값을 잘라내는 메소드
리턴 타입 | 메소드 이름 및 매개 변수 | 설명 |
String | substring(int beginIndex) | 주어진 시작 인덱스부터 문자열 끝까지의 부분 문자열을 반환합니다. |
String | substring(int beginIndex, int endIndex) | 주어진 시작 인덱스부터 종료 인덱스 전까지의 부분 문자열을 반환합니다. |
CharSequence | subSequence(int beginIndex, int endIndex) | 주어진 시작 인덱스부터 종료 인덱스 전까지의 부분 문자열을 CharSequence로 반환합니다. |
public class SubstringExample {
public static void main(String[] args) {
String str = "Hello, World!";
// substring(int beginIndex)
String substr1 = str.substring(7);
System.out.println("str.substring(7): " + substr1); // "World!"
// substring(int beginIndex, int endIndex)
String substr2 = str.substring(7, 12);
System.out.println("str.substring(7, 12): " + substr2); // "World"
// subSequence(int beginIndex, int endIndex)
CharSequence subseq = str.subSequence(7, 12);
System.out.println("str.subSequence(7, 12): " + subseq); // "World"
}
}
str.substring(7): World!
str.substring(7, 12): World
str.subSequence(7, 12): World
13.6.5. 문자열을 여러 개의 문자 배열로 나누는 split 메소드
리턴타입 | 메소드 이름 | 설명 |
String[] | split(String regex) | 문자열을 정규 표현식에 따라 나눔 |
String[] | split(String regex, int limit) | 최대 분할 수만큼 문자열을 정규 표현식에 따라 나눔 |
13.6.5.1. split(String regex) 메소드 예시:
// 문자열을 공백을 기준으로 나누기
String sentence = "Hello, world! How are you?";
String[] words = sentence.split("\\s+");
for (String word : words) {
System.out.println(word);
}
Hello,
world!
How
are
you?
13.6.5.2. split(String regex, int limit) 메소드 예시:
// 문자열을 공백을 기준으로 최대 2번 나누기
String sentence = "Hello, world! How are you?";
String[] words = sentence.split("\\s+", 2);
for (String word : words) {
System.out.println(word);
}
Hello,
world! How are you?
13.7. 문자열 값을 바꾸는 메소드
13.7.1. 문자열을 합치는 메소드와 공백을 없애는 메소드
리턴타입 | 메소드 이름 | 설명 |
String | concat(String str) | 현재 문자열에 다른 문자열을 연결함 |
String | trim() | 문자열의 앞뒤 공백을 제거함 |
13.7.1.1. concat(String str) 메소드 예시:
// 문자열 연결하기
String str1 = "Hello, ";
String str2 = "world!";
String result = str1.concat(str2);
System.out.println(result);
Hello, world!
13.7.1.2. trim() 메소드 예시:
// 문자열 앞뒤 공백 제거하기
String str = " Hello, world! ";
String trimmed = str.trim();
System.out.println(trimmed);
Hello, world!
13.7.2. 내용을 교체(replace)하는 메소드
리턴타입 | 메소드 이름 | 설명 |
String | replace(char oldChar, char newChar) | 문자열 내의 모든 oldChar를 newChar로 대체 |
String | replaceAll(String regex, String replacement) | 문자열 내의 모든 정규 표현식에 일치하는 부분을 대체 문자열로 대체 |
String | replaceFirst(String regex, String replacement) | 문자열 내의 첫 번째 정규 표현식에 일치하는 부분을 대체 문자열로 대체 |
13.7.2.1. replace(char oldChar, char newChar) 메소드 예시:
// 문자열에서 'o'를 '0'으로 대체하기
String str = "Hello, world!";
String replaced = str.replace('o', '0');
System.out.println(replaced);
Hell0, w0rld!
13.7.2.2. replaceAll(String regex, String replacement) 메소드 예시:
// 문자열에서 숫자를 '*'로 대체하기
String str = "Today is 2025-04-21.";
String replaced = str.replaceAll("[0-9]", "*");
System.out.println(replaced);
Today is ****-**-**.
13.7.2.3. replaceFirst(String regex, String replacement) 메소드 예시:
// 문자열에서 첫 번째로 나오는 숫자를 '*'로 대체하기
String str = "2025-04-21 is today's date.";
String replaced = str.replaceFirst("[0-9]", "*");
System.out.println(replaced);
*025-04-21 is today's date.
13.7.3. 특정 형식에 맞춰 값을 치환하는 메소드
리턴타입 | 메소드 이름 | 설명 |
String | format(String format, Object... args) | 지정된 포맷 문자열에 맞게 문자열을 포맷함 |
String | format(Locale l, String format, Object... args) | 지정된 로케일과 포맷 문자열에 맞게 문자열을 포맷함 |
13.7.3.1. format(String format, Object... args) 메소드 예시:
// 포맷 문자열을 사용하여 문자열 포맷팅하기
String name = "Alice";
int age = 30;
String formatted = String.format("Name: %s, Age: %d", name, age);
System.out.println(formatted);
Name: Alice, Age: 30
13.7.3.2. format(Locale l, String format, Object... args) 메소드 예시:
// 로케일을 지정하여 포맷 문자열을 사용하여 문자열 포맷팅하기
String currency = "USD";
double amount = 1000.50;
Locale locale = Locale.US;
String formatted = String.format(locale, "Amount: %.2f %s", amount, currency);
System.out.println(formatted);
Amount: 1000.50 USD
13.7.4. 대소문자 바꾸는 메소드
리턴타입 | 메소드 이름 | 설명 |
String | toLowerCase() | 문자열의 모든 문자를 소문자로 변환함 |
String | toLowerCase(Locale locale) | 지정된 로케일에 따라 문자열의 모든 문자를 소문자로 변환함 |
String | toUpperCase() | 문자열의 모든 문자를 대문자로 변환함 |
String | toUpperCase(Locale locale) | 지정된 로케일에 따라 문자열의 모든 문자를 대문자로 변환함 |
13.7.4.1. toLowerCase() 메소드 예시:
// 문자열을 소문자로 변환하기
String str1 = "Hello, WORLD!";
String lowerCase1 = str1.toLowerCase();
System.out.println(lowerCase1); // 출력 결과: hello, world!
13.7.4.2. toLowerCase(Locale locale) 메소드 예시:
// 로케일을 지정하여 문자열을 소문자로 변환하기
String str2 = "HELLO, WORLD!";
Locale locale = Locale.ENGLISH;
String lowerCase2 = str2.toLowerCase(locale);
System.out.println(lowerCase2); // 출력 결과: hello, world!
13.7.4.3. toUpperCase() 메소드 예시:
// 문자열을 대문자로 변환하기
String str3 = "Hello, world!";
String upperCase1 = str3.toUpperCase();
System.out.println(upperCase1); // 출력 결과: HELLO, WORLD!
13.7.4.4. toUpperCase(Locale locale) 메소드 예시:
// 로케일을 지정하여 문자열을 대문자로 변환하기
String str4 = "Hello, world!";
Locale locale = Locale.ENGLISH;
String upperCase2 = str4.toUpperCase(locale);
System.out.println(upperCase2); // 출력 결과: HELLO, WORLD!
13.7.5. 기본 자료형을 문자열로 변환하는 메소드
리턴타입 | 메소드 이름 및 매개 변수 | 설명 |
String | valueOf(boolean b) | 주어진 boolean 값을 대응하는 Boolean 객체를 반환함 |
String | valueOf(char c) | 주어진 char 값을 대응하는 Character 객체를 반환함 |
String | valueOf(char[] data) | 주어진 문자 배열을 대응하는 String 객체를 반환함 |
String | valueOf(char[] data, int offset, int count) | 주어진 문자 배열의 일부를 대응하는 String 객체를 반환함 |
String | valueOf(double d) | 주어진 double 값을 대응하는 Double 객체를 반환함 |
String | valueOf(float f) | 주어진 float 값을 대응하는 Float 객체를 반환함 |
String | valueOf(int i) | 주어진 int 값을 대응하는 Integer 객체를 반환함 |
String | valueOf(long l) | 주어진 long 값을 대응하는 Long 객체를 반환함 |
String | valueOf(Object obj) | 주어진 객체의 문자열 표현을 반환함 |
public class ValueOfExamples {
public static void main(String[] args) {
// boolean 값을 String 객체로 변환하여 출력
boolean value1 = true;
String booleanStr = String.valueOf(value1);
System.out.println("Boolean: " + booleanStr);
// char 값을 String 객체로 변환하여 출력
char value2 = 'A';
String charStr = String.valueOf(value2);
System.out.println("Character: " + charStr);
// 문자 배열을 String 객체로 변환하여 출력
char[] charArray = {'H', 'e', 'l', 'l', 'o'};
String charArrayStr = String.valueOf(charArray);
System.out.println("Character Array: " + charArrayStr);
// 일부 문자 배열을 String 객체로 변환하여 출력
String partialStr = String.valueOf(charArray, 1, 3);
System.out.println("Partial Character Array: " + partialStr);
// double 값을 String 객체로 변환하여 출력
double value3 = 123.45;
String doubleStr = String.valueOf(value3);
System.out.println("Double: " + doubleStr);
// float 값을 String 객체로 변환하여 출력
float value4 = 12.34f;
String floatStr = String.valueOf(value4);
System.out.println("Float: " + floatStr);
// int 값을 String 객체로 변환하여 출력
int value5 = 123;
String intStr = String.valueOf(value5);
System.out.println("Integer: " + intStr);
// long 값을 String 객체로 변환하여 출력
long value6 = 123456789L;
String longStr = String.valueOf(value6);
System.out.println("Long: " + longStr);
// 객체의 문자열 표현을 String 객체로 변환하여 출력
Object obj = new Object();
String objStr = String.valueOf(obj);
System.out.println("Object: " + objStr);
}
}
Boolean: true
Character: A
Character Array: Hello
Partial Character Array: ell
Double: 123.45
Float: 12.34
Integer: 123
Long: 123456789
Object: java.lang.Object@<해시코드>
13.8. StringBuffer와 StringBuilder에 대한 정의와 차이점
13.8.1. StringBuffer와 StringBuilder 정의:
- StringBuffer:
- StringBuffer 클래스는 가변(mutable)한 문자열을 처리하는 클래스로, 문자열의 추가, 수정, 삭제 등을 할 수 있다.
- StringBuffer는 스레드 안전(thread-safe)하도록 설계되어 있어 여러 스레드에서 동시에 접근해도 안전하게 작동한다.
- StringBuilder:
- StringBuilder 클래스는 마찬가지로 가변(mutable)한 문자열을 처리하는 클래스이지만, StringBuffer와 달리 스레드 안전하지 않다.
- 단일 스레드 환경에서 사용할 때 StringBuffer보다 더 빠르게 동작할 수 있다.
13.8.2. StringBuffer가 Thread-safe하고 StringBuilder가 Thread-safe하지 않은 이유
- StringBuffer의 Thread-safety:
- StringBuffer는 각 메소드에 대해 synchronized 키워드가 적용되어 있어, 여러 스레드에서 동시에 접근해도 내부 데이터에 대한 동기화가 보장된다. 이로 인해 여러 스레드가 동시에 StringBuffer 객체의 메소드를 호출해도 데이터 일관성을 유지할 수 있다.
- StringBuilder의 Thread-safety:
- StringBuilder는 동기화가 되어 있지 않기 때문에, 여러 스레드가 동시에 접근하면 데이터 일관성 문제가 발생할 수 있다. 따라서 StringBuilder는 단일 스레드 환경에서 사용하는 것이 적합하다.
13.8.3. append 메소드 사용법과 더하기 연산 및 반복문에서의 차이점
- append 메소드 사용법: append 메소드는 문자열을 현재 객체의 끝에 추가한다.
StringBuilder sb = new StringBuilder();
sb.append("Hello");
sb.append(" World");
- 더하기 연산과 반복문에서의 차이점:
- 더하기 연산(+)은 각각의 연산마다 새로운 문자열을 생성하기 때문에 반복적인 연산이 있을 경우 성능이 저하될 수 있다.
- JDK 5 이상에서는 문자열의 더하기 연산을 할 경우 컴파일할 때 자동으로 해당 연산을 StringBuffer 혹은 StringBuilder로 변환해 준다. 따라서, 일일이 더하는 작업을 변환해 줄 필요는 없다.
- 반복문에서의 사용: 문자열을 반복적으로 더할 때는 StringBuilder 또는 StringBuffer를 사용하여 성능을 개선할 수 있다.
- for 루프와 같이 반복 연산을 하는 경우에는 자동으로 변환해주지 않는다.
13.8.4. CharSequence 타입 매개 변수 사용 이유
- CharSequence, StringBuffer, StringBuilder 중 하나의 클래스를 사용하여 매개 변수로 받는 경우 CharSequence 타입으로 받는게 좋다.
- CharSequence는 문자열의 추상 인터페이스로, String, StringBuilder, StringBuffer 등 문자열을 표현하는 다양한 클래스들이 CharSequence 인터페이스를 구현하고 있기 때문에, 이들 클래스를 통합하여 사용할 수 있다. 이는 유연성과 호환성을 제공하며, 코드의 재사용성을 높이는 데 도움이 된다.
13.8.5. StringBuffer vs StringBuilder 사용 시기
- StringBuffer 사용 시기:
- 멀티 스레드 환경에서 동시 접근이 있을 경우, 데이터 일관성을 보장하기 위해 StringBuffer를 사용해야 한다.
- StringBuilder 사용 시기:
- 단일 스레드 환경에서 성능을 최적화하고자 할 때, 또는 스레드 동기화가 필요하지 않은 경우에 StringBuilder를 사용한다.
13.8.6. 예제 코드
public class StringBufferStringBuilderExample {
// 공유 데이터
static StringBuilder sharedStringBuilder = new StringBuilder();
static StringBuffer sharedStringBuffer = new StringBuffer();
public static void main(String[] args) {
// 스레드 생성
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
sharedStringBuilder.append("A");
}
});
Thread thread2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
sharedStringBuilder.append("B");
}
});
// 스레드 시작
thread1.start();
thread2.start();
// 스레드가 종료될 때까지 기다림
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 결과 출력
System.out.println("StringBuilder 결과 길이: " + sharedStringBuilder.length()); // 예상 출력: 2000
// StringBuffer 예제
Thread thread3 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
sharedStringBuffer.append("C");
}
});
Thread thread4 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
sharedStringBuffer.append("D");
}
});
// 스레드 시작
thread3.start();
thread4.start();
// 스레드가 종료될 때까지 기다림
try {
thread3.join();
thread4.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 결과 출력
System.out.println("StringBuffer 결과 길이: " + sharedStringBuffer.length()); // 예상 출력: 2000
}
}
14. 클래스 안의 클래스 중첩 클래스(Nested Class)
14.1. 중첩 클래스(Nested Class)를 사용하는 이유
- 한 곳에서만 사용되는 클래스를 논리적으로 묶어서 처리할 필요가 있을 때 (Static Class)
- 캡슐화가 필요할 때(예를 들어 A라는 클래스에 private 변수가 있다. 이 변수에 접근하고 싶은 B라는 클래스를 선언하고, B 클래스를 외부에 노출시키고 싶지 않을 경우가 여기에 속한다). 즉, 내부 구현을 감추고 싶을 때를 말한다. (Inner Class)
- 소스의 가독성과 유지보수성을 높이고 싶을 때 (Inner Class)
14.2. Nested Class 종류 및 특징
14.2.1. Static Nested Class
- static으로 선언된 내부 클래스
- 외부 클래스의 static 멤버만 접근 가능
- 인스턴스 없이 외부 클래스와 독립적으로 사용 가능
package c.inner;
public class OuterOfStatic {// 1
static class StaticNested { // 2
private int value=0;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value=value;
}
}
}
package c.inner;
public class NestedSample {
public static void main(String[] args) {
NestedSample sample=new NestedSample();
sample.makeStaticNestedObject();
}
public void makeStaticNestedObject() {
OuterOfStatic.StaticNested staticNested=
new OuterOfStatic.StaticNested();
staticNested.setValue(3);
System.out.println(staticNested.getValue());
}
}
public class Outer {
static int outerStatic = 100;
static class StaticNested {
void display() {
System.out.println("Accessing static outer value: " + outerStatic);
}
}
public static void main(String[] args) {
Outer.StaticNested nested = new Outer.StaticNested();
nested.display();
}
}
14.2.2. Inner Class (비정적 중첩 클래스)
- 외부 클래스의 인스턴스가 필요
- 외부 클래스의 모든 멤버 (static/instance) 에 접근 가능
package c.inner;
public class OuterOfInner {
class Inner {
private int value=0;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value=value;
}
}
}
package c.inner;
public class InnerSample {
public static void main(String args[]) {
InnerSample sample = new InnerSample();
sample.makeInnerObject();
}
public void makeInnerObject() {
OuterOfInner outer=new OuterOfInner();
OuterOfInner.Inner inner=outer.new Inner();
inner.setValue(3);
System.out.println(inner.getValue());
}
}
public class Outer {
private int number = 42;
class Inner {
void printNumber() {
System.out.println("Outer number: " + number);
}
}
public static void main(String[] args) {
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.printNumber();
}
}
14.3. 한 곳에서만 사용되는 클래스를 논리적으로 묶어서 처리할 필요가 있을 때
14.3.1. 사용하는 이유
- 작은 기능 단위의 클래스가 오직 하나의 외부 클래스에서만 쓰일 때, 별도 파일로 분리하면 오히려 가독성과 관리가 떨어짐
- 외부 클래스와 강하게 결합된 개념이므로 “여기 안에 있다”는 사실이 의미 전달에 도움
14.3.2. 예시 시나리오
- Order 라는 외부 클래스 안에서만 쓰이는 OrderValidator라는 검증용 헬퍼 클래스
14.3.3. 설명
- OrderValidator 는 외부에 노출할 필요 없이 Order 내부에 숨겨두어, 논리적 응집도를 높임
- private static 으로 설정해 외부에서 직접 생성·사용 불가
// Order.java
public class Order {
private String itemId;
private int quantity;
public Order(String itemId, int quantity) {
this.itemId = itemId;
this.quantity = quantity;
}
public boolean validate() {
// OrderValidator는 오직 Order.validate()에서만 사용
OrderValidator validator = new OrderValidator();
return validator.isValidItem(itemId) && validator.isValidQuantity(quantity);
}
// static nested 클래스: Order와 논리적으로 묶이지만,
// 인스턴스 변수 없이 오직 static 멤버(또는 파라미터)를 통해만 동작
private static class OrderValidator {
// 예시: 실제로는 DB나 외부 API 호출 등을 통해 검증
boolean isValidItem(String itemId) {
return itemId != null && itemId.matches("[A-Z0-9]+");
}
boolean isValidQuantity(int qty) {
return qty > 0 && qty <= 100;
}
}
public static void main(String[] args) {
Order order = new Order("ABC123", 5);
System.out.println("Order valid? " + order.validate());
}
}
14.4. 캡슐화가 필요할 때 (내부 구현을 감추고 싶을 때)
14.4.1. 사용하는 이유
- 복잡한 내부 로직(헬퍼, 콜백, 이벤트 리스너 등)을 외부에 노출시키지 않고 감쌀 수 있음
- API 사용자는 외부 클래스의 퍼블릭 메서드만 보면 되고, 내부 구조는 신경 쓰지 않아도 됨
14.4.2. 예시 시나리오
- DataProcessor 가 데이터 변환 중에만 사용하는 Transformer 를 inner 클래스 형태로 숨긴 경우
// DataProcessor.java
public class DataProcessor {
private String rawData;
public DataProcessor(String data) {
this.rawData = data;
}
// 퍼블릭 API
public String process() {
Transformer transformer = new Transformer();
return transformer.transform(rawData);
}
// inner 클래스: 외부 클래스의 인스턴스 변수·메서드에 자유롭게 접근 가능
private class Transformer {
String transform(String input) {
// 외부 클래스의 rawData에 접근 예시
String base = DataProcessor.this.rawData;
// 단순 예시: 모두 대문자로 바꾸고 뒤집기
String upper = input.toUpperCase();
return new StringBuilder(upper).reverse().toString();
}
}
public static void main(String[] args) {
DataProcessor proc = new DataProcessor("hello");
System.out.println(proc.process()); // OLLEH
}
}
14.4.3. 설명
- Transformer 를 private class 로 숨겨, DataProcessor 외부에서 직접 사용할 수 없도록 캡슐화
- 내부 구현을 감추면서도 외부 API(process())는 깔끔하게 관리할 수 있음
14.5. 소스의 가독성과 유지보수성을 높이고 싶을 때
14.5.1. 사용하는 이유
- 관련된 코드를 한 파일, 한 섹션에 몰아넣어 전체적인 흐름 파악이 편해짐
- 클래스 수 과다로 패키지가 복잡해지는 것을 방지
- 변경 시 연관 코드가 한곳에 모여 있어 수정 용이
14.5.2. 예시 시나리오
- GUI 이벤트 처리기, 빌더 패턴 구현 등 작은 헬퍼 클래스를 외부 파일이 아닌 내부에 묶어두는 경우
// Notification.java
public class Notification {
private String message;
private Priority priority;
public enum Priority { LOW, MEDIUM, HIGH }
private Notification(String message, Priority priority) {
this.message = message;
this.priority = priority;
}
// 빌더 패턴으로 객체 생성
public static class Builder {
private String msg;
private Priority pr = Priority.MEDIUM; // 기본값
public Builder setMessage(String m) {
this.msg = m;
return this;
}
public Builder setPriority(Priority p) {
this.pr = p;
return this;
}
public Notification build() {
// 검증 로직 등
if (msg == null || msg.isEmpty()) {
throw new IllegalStateException("Message cannot be empty");
}
return new Notification(msg, pr);
}
}
@Override
public String toString() {
return "[" + priority + "] " + message;
}
public static void main(String[] args) {
Notification note = new Notification.Builder()
.setMessage("Server down!")
.setPriority(Priority.HIGH)
.build();
System.out.println(note);
}
}
14.5.3. 설명
- Builder 클래스를 내부에 두어, Notification 생성 로직과 밀접하게 유지
- 전체 구현을 한 파일에서 볼 수 있어 가독성, 유지보수성 향상
14.6. static nested 클래스 vs. inner 클래스
구분 | static nested 클래스 | inner 클래스 |
선언 방식 | static class X { … } | class X { … } (static 키워드 없음) |
외부 멤버 접근 | 오직 외부 클래스의 static 멤버만 접근 | 외부 클래스의 모든(인스턴스+static) 멤버 접근 가능 |
인스턴스 생성 | Outer.X x = new Outer.X(); | Outer outer = new Outer(); outer.new X(); |
메모리 상 클래스 | 독립된 클래스처럼 동작 | 바깥(Outer) 인스턴스와 항상 연관 |
14.7. 익명클래스 (Anonymous Inner Class)
14.7.1. 일반적인 익명 클래스 사용방법
package c.inner;
public interface EventListener {
public void onClick();
}
package c.inner;
public class MagicButton {
public MagicButton() {
}
private EventListener listener;
public void setListener(EventListener listener) {
this.listener=listener;
}
public void onClickProcess() {
if(listener!=null) {
listener.onClick();
}
}
}
package c.inner;
public class AnonymousSample {
public static void main(String args[]) {
AnonymousSample sample = new AnonymousSample();
sample.setButtonListener();
}
public void setButtonListener() {
MagicButton button=new MagicButton();
MagicButtonListener listener=new MagicButtonListener();
button.setListener(listener);
button.onClickProcess();
}
}
class MagicButtonListener implements EventListener {
public void onClick() {
System.out.println("Magic Button Clicked !!!");
}
}
14.7.2. 인터페이스를 직접 선언하는 익명 클래스 사용방법
package c.inner;
public interface EventListener {
public void onClick();
}
package c.inner;
public class MagicButton {
public MagicButton() {
}
private EventListener listener;
public void setListener(EventListener listener) {
this.listener=listener;
}
public void onClickProcess() {
if(listener!=null) {
listener.onClick();
}
}
}
package c.inner;
public class AnonymousSample {
public static void main(String args[]) {
AnonymousSample sample = new AnonymousSample();
sample.setButtonListenerAnonymous();
}
public void setButtonListenerAnonymous() {
MagicButton button=new MagicButton();
button.setListener(new EventListener() {
public void onClick() {
System.out.println("Magic Button Clicked !!!");
}
});
button.onClickProcess();
}
}
- 객체를 클래스 내에서 재사용 하려면
package c.inner;
public class AnonymousSample {
public static void main(String args[]) {
AnonymousSample sample = new AnonymousSample();
sample.setButtonListenerAnonymousObject();
}
public void setButtonListenerAnonymousObject() {
MagicButton button=new MagicButton();
EventListener listener=new EventListener() {
public void onClick() {
System.out.println("Magic Button Clicked !!!");
}
};
button.setListener(listener);
button.onClickProcess();
}
}
14.8. 내부 클래스에서 외부 변수에 접근하는 범위
- static 클래스는 외부의 static 변수에만 접근이 가능하고 inner 클래스는 static을 포함한 모든 변수에 접근이 가능하다.
package c.inner;
public interface EventListener {
public void onClick();
}
package c.inner;
public class NestedValueReference {
public int publicInt=0;
protected int protectedInt=1;
int justInt=2;
private int privateInt=3;
static int staticInt=4;
static class StaticNested {
public void setValue() {
staticInt=14;
}
}
class Inner {
public void setValue() {
publicInt=20;
protectedInt=21;
justInt=22;
privateInt=23;
staticInt=24;
}
}
public void setValue() {
EventListener listener=new EventListener() {
public void onClick() {
publicInt=30;
protectedInt=31;
justInt=32;
privateInt=33;
staticInt=34;
}
};
}
}
- 내부 함수에서 내부 클래스 static, 일반 내부 클래스에 선언된 private 변수에 접근이 가능하다.
package c.inner;
public class ReferenceAtNested {
static class StaticNested {
private int staticNestedInt=99;
}
class Inner {
private int innerValue=100;
}
public void setValue(int value) {
StaticNested nested=new StaticNested();
nested.staticNestedInt=value;
Inner inner=new Inner();
inner.innerValue=value;
}
}
15. 어노테이션 (Annotation)
15.1. 어노테이션의 특성과 사용 시점
15.1.1. 어노테이션이란
- 정의:
- 어노테이션은 @ 기호로 시작하는 인터페이스 형태의 자바 타입으로, 소스 코드에 메타데이터(추가 정보)를 부여한다.
- 특징:
- 어노테이션 자체가 프로그램의 논리를 변경하지 않으며, 컴파일 타임 또는 런타임 도구가 해석해 별도 동작을 수행한다.
15.1.2. 어노테이션 사용 시기
- 컴파일러 정보 제공:
- 어노테이션을 통해 컴파일러가 오류를 검출하거나 경고를 억제하도록 지시할 수 있다.
- 컴파일·배포 시 처리 지정:
- apt나 배포 도구가 어노테이션 정보를 읽어 코드, XML 등을 자동 생성하거나 구성을 변경할 수 있다.
- 런타임 처리:
- 리플렉션을 통해 런타임에 어노테이션 정보를 읽고 별도의 로직을 수행할 수 있다.
15.2. 표준 어노테이션
15.2.1. @Override
- 설명:
- 상위 타입(supertype)에 선언된 메서드를 재정의함을 명시한다.
- 효과:
- 잘못된 시그니처로 재정의할 경우 컴파일 오류가 발생해 실수를 예방한다.
- 사용 예시:
public class BaseService {
public void execute() { /*...*/ }
}
public class UserService extends BaseService {
@Override // BaseService의 execute()를 재정의함을 컴파일러가 검증
public void execute() {
System.out.println("UserService 실행");
}
}
// Animal.java
public class Animal {
public void speak() {
System.out.println("Generic sound");
}
}
// Dog.java
public class Dog extends Animal {
@Override // 잘못된 시그니처를 잡아내어 컴파일 오류 발생
public void speek() {
System.out.println("Woof");
}
}
15.2.2. @Deprecated
- 설명:
- 해당 요소(클래스, 메서드, 필드 등)가 더 이상 사용되지 않거나 위험하므로 사용을 권장하지 않음을 표시한다.
- 효과:
- 해당 요소 사용 시 컴파일 경고가 발생하며, Javadoc @deprecated 태그와 함께 후속 대체 방법을 안내할 수 있다.
- 오래된 메서드나 클래스 위에 달아 유지보수 방향을 안내한다.
- 사용 예시:
public class LegacyProcessor {
/**
* @deprecated 이 메서드는 성능 이슈로 인해 제거될 예정입니다.
* 대신 processNew()를 사용하세요.
*/
@Deprecated
public void processOld() {
// ...
}
public void processNew() {
// 최적화된 신규 처리 로직
}
}
public class LegacyUtils {
/**
* @deprecated since 2.0
* Use newMethod() instead.
*/
@Deprecated
public static void oldMethod() {
// ...
}
public static void newMethod() {
// 향상된 구현
}
}
15.2.3. @SuppressWarnings
- 설명:
- 컴파일러 경고를 억제(suppress)하기 위해 사용하며, 문자열 키로 특정 경고를 지정한다.
- 효과:
- 불필요하거나 불가피한 경고를 제거해 경고 목록을 깔끔하게 유지한다.
- 사용 예시:
@SuppressWarnings("unchecked") // 제네릭 형변환 경고 억제
public void legacyApiWrapper() {
List rawList = getRawList(); // 제네릭 미사용 코드
List<String> names = (List<String>) rawList;
// ...
}
public class WarningDemo {
@SuppressWarnings("unchecked") // unchecked 경고 억제
public void legacyCast() {
java.util.List rawList = new java.util.ArrayList();
rawList.add("test");
java.util.List<String> stringList = rawList; // unchecked
}
}
15.3. 메타 어노테이션
- 자바에는 어노테이션을 선언할 때 적용 대상과 유지 정책 등을 지정하기 위한 네 가지 메타 어노테이션이 있다.
메타 어노테이션 | 설명 |
@Target | 어떤 코드 요소에 적용 가능한지 지정합니다 |
@Retention | 어노테이션 정보를 언제까지 유지할지 지정합니다 |
@Documented | Javadoc 생성 시 어노테이션 정보를 문서에 포함하도록 지정합니다 |
@Inherited | 클래스 상속 시 서브클래스에 어노테이션이 자동으로 상속되도록 지정합니다 |
15.3.1. @Target
15.3.1.1. 역할:
- java.lang.annotation.ElementType 열거형 값으로 어느 프로그램 요소에 적용될 수 있는지 제한한다.
15.3.1.2. 괄호 안에 적용 가능한 대상:
요소 유형 (ElementType) | 설명 |
TYPE | 클래스, 인터페이스, 열거형 선언 |
FIELD | 필드(멤버 변수) 선언 |
METHOD | 메서드 선언 |
PARAMETER | 메서드 매개변수 선언 |
CONSTRUCTOR | 생성자 선언 |
LOCAL_VARIABLE | 로컬 변수 선언 |
ANNOTATION_TYPE | 어노테이션 타입 선언 |
PACKAGE | 패키지 선언 |
TYPE_PARAMETER (Java 8+) | 제네릭 타입 매개변수 선언 |
TYPE_USE (Java 8+) | 타입 사용 위치 (캐스트, 변수 선언 등) |
15.3.2. @Retention
15.3.1.1. 역할:
- 어노테이션 정보를 언제까지 유지할지를 지정한다.
15.3.1.2. 괄호 안에 적용 가능한 대상:
유지 정책 (RetentionPolicy) | 설명 |
SOURCE | 컴파일러가 무시, .java 파일에만 존재 어노테이션 정보가 컴파일시 사라짐 |
CLASS (기본값) | .class 파일에 기록되나 런타임 시 리플렉션 불가 클래스 파일에 있는 어노테이션 정보가 컴파일러에 의해서 참조 가능함. 하지만, 가상머신에서는 사라짐 |
RUNTIME | .class 파일에 기록되고 런타임에 리플렉션으로 접근 가능 |
15.3.2. @Retention
15.3.2.1. 역할:
- 해당 어노테이션이 javadoc 생성 시 문서에 포함되도록 한다.
15.3.2.2. 특징:
- 기본적으로 커스텀 어노테이션은 Javadoc에 나타나지 않으므로, API 문서에 노출하려면 이 메타 어노테이션을 함께 선언해야 한다.
15.3.3. @Inherited
15.3.3.1. 역할:
- 클래스에 붙은 어노테이션을 서브클래스가 상속받도록 지정합니다(기본적으로 어노테이션은 상속되지 않음)
15.3.3.2. 특징:
- 메서드나 필드에는 적용되지 않으며, 오직 클래스 선언에만 유효하다.
15.4. 커스텀 어노테이션 선언하기
15.4.1. 기본 문법
import java.lang.annotation.*;
// ① 메타 어노테이션으로 적용 범위와 유지 정책 지정
@Target(ElementType.METHOD) // 메서드에만 적용
@Retention(RetentionPolicy.RUNTIME)// 런타임까지 유지
@Documented // Javadoc에 포함
public @interface Audit {
String value() default ""; // 요소 속성(이름: value)
String[] tags() default {}; // 배열 속성
}
15.4.2. 선언 시 필요한 지식
- @interface 키워드로 어노테이션 선언
- 메타 어노테이션 적용: 어떤 요소에, 어떤 시점까지 유지할지 지정해야 함
- 요소 타입 제약: 반환 타입은 원시타입, String, Class, 열거형, 어노테이션, 배열만 허용
- 기본값 설정: default 키워드로 요소의 기본값 지정
15.4.3. 실무 기반 예제
- 이 예제에서 @Audit 어노테이션은 클래스와 메서드에 적용되며, 런타임까지 유지되어 리플렉션으로 읽어와 로그나 트래킹 기능에 활용할 수 있다.
package com.example.annotations;
import java.lang.annotation.*;
// 1. 메타 어노테이션 지정
@Documented
@Retention(RetentionPolicy.RUNTIME) // 런타임까지 유지
@Target({ElementType.TYPE, ElementType.METHOD})
@Inherited
public @interface Audit {
String author(); // 작성자
String date(); // 작성 일자(YYYY-MM-DD)
Priority level() default Priority.MEDIUM; // 우선순위 기본 MEDIUM
enum Priority { LOW, MEDIUM, HIGH }
}
// 2. 어노테이션 적용 예시
@Audit(author = "홍길동", date = "2025-04-21", level = Audit.Priority.HIGH)
public class PaymentService {
@Audit(author = "이순신", date = "2025-04-21")
public void processPayment(String userId, double amount) {
// 결제 처리 로직
System.out.println("Processing payment for " + userId + ": " + amount);
}
}
// 3. 런타임 리플렉션 처리 예시
package com.example.processor;
import com.example.annotations.Audit;
import java.lang.reflect.*;
public class AuditProcessor {
public static void main(String[] args) throws Exception {
Class<?> cls = Class.forName("com.example.annotations.PaymentService");
// 클래스 레벨 어노테이션 확인
if (cls.isAnnotationPresent(Audit.class)) {
Audit audit = cls.getAnnotation(Audit.class);
System.out.println("Class Audit: " + audit.author() + ", " + audit.date());
}
// 메서드 레벨 어노테이션 확인
for (Method m : cls.getDeclaredMethods()) {
if (m.isAnnotationPresent(Audit.class)) {
Audit audit = m.getAnnotation(Audit.class);
System.out.println("Method " + m.getName() + " Audit: " + audit.author() + ", " + audit.date());
}
}
}
}
- reference :
☕ 자바 제네릭(Generics) 개념 & 문법 정복하기
제네릭 (Generics) 이란 자바에서 제네릭(Generics)은 클래스 내부에서 사용할 데이터 타입을 외부에서 지정하는 기법을 의미한다. 객체별로 다른 타입의 자료가 저장될 수 있도록 한다. 자바에서 배
inpa.tistory.com