
개념
특징
String 객체의 내부 구성 요소
public final class String implements java.io.Serializable, Comparable {
private final byte[] value;
}
자바 언어에서 String을 불변으로 설정한 이유
개념
특징
개념
특징
StringBuffer 객체의 내부 구성 요소
public final class StringBuffer implements java.io.Serializable {
private byte[] value;
}
| 특징 | String | StringBuilder | StringBuffer |
|---|---|---|---|
| 가변성 | 불변 | 가변 | 가변 |
| 성능 | 느림(새 객체 생성) | 가장 빠름 | 느림(동기화 비용) |
| Thread-safe | 멀티 스레드 환경에 안전 | 멀티 스레드 환경에 안전하지 않음 | 멀티 스레드 환경에 안전 |
| 멀티스레드 | 사용 가능하지만 비효율적 | 동기화가 안되기 때문에 사용 불가능 | 사용 가능 |
→ 값 변경, 메서드 오버라이딩, 상속을 방지하는 역할을 한다.
try {
① 예외가 발생할 수 있는 코드;
} catch (발생할 수 있는 예외 타입) {
② 예외처리 코드;
} finally {
③ 예외와 상관없이 무조건 실행되는 코드;
}
예시
protected void finalize() throws Throwable {
// 객체 소멸 전에 필요한 처리
super.finalize();
}
체크 예외 (Checked Exception) / 언체크 예외 (Unchecked Exception)
대표적인 체크 예외
체크예외는 컴파일러가 예외처리를 강제하는 예외이기 때문에 개발자가 실수로 예외처리를 누락하지 않도록 도와준다.
→ 하지만 개발자가 모든 체크 예외를 처리해줘야 하고 신경쓰고 싶지 않은 예외까지 처리해야 된다는 단점이 있다.
실제 애플리케이션을 개발할 때 발생하는 에외 들은 복구가 불가능한 경우가 많기 때문에 대부분 언체크 예외를 사용한다.
대표적인 언체크 예외
예외 복구, 예외 처리 회피, 예외 전환
ex) catch 블록에서 새로운 예외 던지기
public class ExceptionTranslation {
public static void main(String[] args) {
try {
validateAge(-5);
} catch (IllegalArgumentException e) {
System.out.println("예외 발생: " + e.getMessage());
}
}
public static void validateAge(int age) {
try {
if (age < 0) {
throw new ArithmeticException("나이는 음수가 될 수 없습니다."); // 원래 예외 발생
}
} catch (ArithmeticException e) {
throw new IllegalArgumentException("잘못된 나이 입력: " + age, e); // 의미 있는 예외로 변환
}
}
}
equals와 hashCode는 모든 Java 객체의 부모 객체인 Object 클래스에 정의되어 있다. 그렇기 때문에 Java의 모든 객체는 Object 클래스에 정의된 equals와 hashCode 함수를 상속받고 있다. 두 메서드 모두 객체의 동등성 비교와 컬렉션에서의 활용을 위해 매우 중요한 메서드이다.
equals() 기본 구현
public boolean equals(Object obj) {
return (this == obj);
}
equals() 오버라이딩
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && name.equals(person.name);
}
}
public class EqualsTest {
public static void main(String[] args) {
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);
System.out.println(p1 == p2); // false (주소 비교)
System.out.println(p1.equals(p2)); // true (내용 비교)
}
}
→ 동일성을 비교하는 equals 메소드를 호출해보면 true가 나오는데, 그 이유는 String 클래스에서 equals 메소드를 오버라이드하여 객체가 같은 값을 갖는지 동등성(Equality)을 비교하도록 처리했기 때문이다.
class Person {
String name;
public Person(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person("홍길동");
Person p2 = new Person("홍길동");
// 객체 인스턴스마다 각기 다른 주해시코드(주소))를 가지고 있다.
System.out.println(p1.hashCode()); // 622488023
System.out.println(p2.hashCode()); // 1933863327
}
}
hashCode() 정의
public native int hashCode();
→ native 키워드는 메소드가 JNI(Java Native Interface)라는 native code를 이용해 구현되었음을 의미한다.
hashCode() 오버라이딩
import java.util.Objects;
class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return age == person.age && name.equals(person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age); // name과 age를 기반으로 해시코드 생성
}
}
public class HashCodeTest {
public static void main(String[] args) {
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // true (같은 객체로 인식)
}
}
→ 객체의 주소가 아닌 객체의 필드의 값을 비교하기 위해 equals() 를 오버라이딩 시킨다면 당연히hashCode도 같이 객체의 필드를 다루도록 오버라이딩 해야된다. 왜냐하면 equals() 의 결과가 true 인 두 객체의 해시코드는 반드시 같아야한다는자바의 규칙 때문이다.
hashCode()는 객체를 빠르게 찾기 위한 값일 뿐 equals()가 true인지는 직접 확인하는게 좋다.
Single Responsibility Principle
Open / Close Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
자바에서 데이터를 저장할 때 원시타입, 참조 타입 두가지 방식으로 관리한다.
→ 성능 관점에서는 원시 타입의 성능이 참조 타입보다 우수하다.
int a = 10; // 원시 타입 → 스택에 직접 저장된다.
Integer b = 10; // 참조 타입 → 힙에 Integer 객체 생성 후에 저장된다.
| 원시타입이 사용하는 메모리 | 참조타입이 사용하는 메모리 |
|---|---|
| boolean - 1bit | Boolean – 128 bits |
| byte - 8bits | Byte - 128bits |
| short, cagr - 16bits | Short, Charater - 128bits |
| int, float - 32bits | Integer, Float - 128bits |
| long, double - 64bits | Long, Double - 196bits |
→ 메모리 관점에서도 원시 타입이 참조 타입보다 효율적이다.
int x = null; // 컴파일 에러 발생
Integer y = null; // 문제 없음
→ 그렇기 때문에 참조 타입을 사용할 때는 반드시 NULL을 체크 해야한다.
제네릭에서는 원시 타입을 직접 사용할 수 없고 반드시 참조 타입을 사용해야 한다.
// 불가능
List<i> list;
// 가능
List<Integer> list;