Object 클래스는 자바에서 모든 클래스의 최상위 부모 클래스입니다. 즉, 자바의 모든 클래스는 암시적으로 Object 클래스를 상속받고 있으며, 이를 통해 기본적인 동작을 모든 클래스에 제공할 수 있습니다. 자바에서 정의된 모든 클래스는 Object 클래스로부터 공통적인 메소드와 기능을 상속받아 사용하게 됩니다.
1. Object 클래스란?
최상위 클래스: 자바의 모든 클래스는 Object 클래스를 상속받습니다. 이는 직접적으로 또는 간접적으로 모든 클래스가 Object 클래스의 서브클래스가 된다는 의미입니다.
기본 제공 메소드: Object 클래스는 모든 객체가 사용할 수 있는 기본 메소드를 제공하며, 이 메소드들은 객체의 기본적인 동작(비교, 출력, 해시코드 등)을 정의합니다.
Object 클래스의 주요 메소드
1. equals(Object obj)
edu.ch9.objectClassMethod.equals 패키지
객체의 동등성을 비교하는 메소드입니다. 기본적으로는 참조 값(메모리 주소)을 비교하지만, 개발자가 이 메소드를 오버라이드하여 객체의 상태 값을 기준으로 비교할 수 있습니다.
public class Person {
String name;
Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Person person = (Person) obj;
return this.name.equals(person.name);
}
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person("Alice");
Person p2 = new Person("Alice");
System.out.println(p1.equals(p2)); // 출력: true
}
}
위 코드에서 equals() 메소드를 오버라이드하여 객체의 name 필드를 기준으로 동등성을 비교했습니다.
2. hashCode()
객체의 해시코드 값을 반환(각 인스턴스 고유의 메모리 위치값을 정수로 반환)하는 메소드로, 동등한 객체는 동일한 해시코드를 반환해야 합니다.
hashCode()와 equals()는 함께 오버라이드하는 것이 일반적입니다. 특히 HashMap이나 HashSet과 같은 컬렉션에서 객체를 저장하거나 검색할 때 hashCode()가 사용됩니다.
@Override
public int hashCode() {
return Objects.hash(name); // name 필드를 기준으로 해시코드 반환
}
3. toString()
edu.ch9.objectClassMethod.toString 패키지
기본적으로는 클래스명과 해시값을 반환하는 메소드입니다.
println 메소드로 객체 출력시 기본적으로 이 메소드의 결과값 출력
객체의 문자열 표현을 반환하는 메소드지만 이를 오버라이드하여 객체의 유의미한 정보를 반환하도록 커스터마이징할 수 있습니다.
public class Person {
String name;
Person(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person[name=" + name + "]";
}
}
public class Main {
public static void main(String[] args) {
Person p1 = new Person("Alice");
System.out.println(p1); // 출력: Person[name=Alice]
}
}
위의 예시에서는 toString()을 오버라이드하여 객체의 의미 있는 정보를 출력하도록 했습니다.
Wrapper 클래스
Wrapper 클래스는 기본 자료형(Primitive types)을 객체로 다루기 위해 제공되는 자바의 클래스입니다. 자바에는 8가지 기본 자료형이 있으며, 이 기본 자료형들은 객체가 아니기 때문에 메소드 호출이나 컬렉션 프레임워크와 같은 곳에서 직접 사용할 수 없습니다. 이러한 문제를 해결하기 위해 Wrapper 클래스가 도입되었습니다.
1. Wrapper 클래스란?
Wrapper 클래스는 기본 자료형을 객체로 감싸는 클래스입니다.
기본 자료형을 객체로 다룰 수 있도록 도와줍니다.
자바의 컬렉션 클래스(ArrayList, HashMap 등)과 같은 클래스들은 객체만 다룰 수 있으므로, 기본 자료형을 객체로 변환해야 할 때 Wrapper 클래스를 사용합니다.
2. 기본 자료형과 대응되는 Wrapper 클래스
자바에서는 각 기본 자료형에 대응되는 Wrapper 클래스가 존재합니다. 다음은 기본 자료형과 그에 대응하는 Wrapper 클래스입니다.
기본 자료형
Wrapper 클래스
byte
Byte
short
Short
int
Integer
long
Long
float
Float
double
Double
char
Character
boolean
Boolean
3. Wrapper 클래스의 사용 예시
박싱(Boxing)
기본 자료형을 Wrapper 객체로 변환하는 과정입니다.
예를 들어, int를 Integer 객체로 변환하는 것이 박싱입니다.
int num = 100;
Integer boxedNum = Integer.valueOf(num); // 박싱
언박싱(Unboxing)
Wrapper 객체를 기본 자료형으로 변환하는 과정입니다.
예를 들어, Integer 객체를 int로 변환하는 것이 언박싱입니다.
Integer wrapperInt = Integer.valueOf(200);
int num = wrapperInt.intValue(); // 언박싱
기본 자료형을 객체로 변환
Wrapper 클래스를 사용하여 기본 자료형을 객체로 변환할 수 있습니다.
edu.ch9.wrapper 패키지
public class WrapperExample {
public static void main(String[] args) {
// 기본 자료형
int primitiveInt = 5;
// 기본 자료형을 Wrapper 객체로 변환 (박싱)
Integer wrapperInt = Integer.valueOf(primitiveInt);
// Wrapper 객체를 기본 자료형으로 변환 (언박싱)
int unboxedInt = wrapperInt.intValue();
System.out.println("Wrapper 객체: "
+ wrapperInt); // 출력: Wrapper 객체: 5
System.out.println("언박싱된 값: "
+ unboxedInt); // 출력: 언박싱된 값: 5
}
}
4. 오토박싱과 오토언박싱
오토박싱(auto-boxing)과 오토언박싱(auto-unboxing)을 통해 기본 자료형과 Wrapper 클래스 간의 변환이 자동으로 이루어집니다. 이로 인해 개발자는 명시적으로 valueOf()나 intValue() 같은 메소드를 호출하지 않아도 됩니다.
오토박싱: 기본 자료형을 자동으로 Wrapper 객체로 변환하는 과정.
edu.ch9.wrapper 패키지
public class AutoBoxingExample {
public static void main(String[] args) {
// 오토박싱: 기본 자료형이 자동으로 Wrapper 객체로 변환
Integer wrapperInt = 10; // 자동으로 Integer.valueOf(10)을 호출한 것과 같음
System.out.println("Wrapper 객체: "
+ wrapperInt); // 출력: Wrapper 객체: 10
}
}
오토언박싱: Wrapper 객체를 자동으로 기본 자료형으로 변환하는 과정.
edu.ch9.wrapper 패키지
public class AutoUnboxingExample {
public static void main(String[] args) {
Integer wrapperInt = 20;
// 오토언박싱: Wrapper 객체가 자동으로 기본 자료형으로 변환
int primitiveInt = wrapperInt; // 자동으로 wrapperInt.intValue()를 호출한 것과 같음
System.out.println("언박싱된 값: " + primitiveInt); // 출력: 언박싱된 값: 20
}
}
int intVal = intObj.intValue();
double doubleVal = doubleObj.doubleValue();
6. Wrapper 클래스의 활용 예시
1. 컬렉션 클래스와의 사용
Wrapper 클래스를 사용하면 기본 자료형을 컬렉션 클래스와 함께 사용할 수 있습니다. 기본 자료형은 컬렉션에 직접 넣을 수 없기 때문에, Wrapper 클래스가 필요합니다.
edu.ch9.wrapper 패키지
import java.util.ArrayList;
public class CollectionExample {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
// 오토박싱: 기본 자료형을 컬렉션에 추가
list.add(10);
list.add(20);
// 오토언박싱: 컬렉션에서 꺼낸 값을 기본 자료형으로 변환
int firstValue = list.get(0);
System.out.println("첫 번째 값: " + firstValue); // 출력: 첫 번째 값: 10
}
}
각 기본 자료형(int, double, boolean 등)에 대응하는 Wrapper 클래스가 있습니다.
박싱은 기본 자료형을 객체로 변환하는 과정이고, 언박싱은 Wrapper 객체를 기본 자료형으로 변환하는 과정입니다.
자바 5 이후에는 오토박싱과 오토언박싱 기능을 통해 기본 자료형과 Wrapper 객체 간의 변환이 자동으로 이루어집니다.
제네릭
제네릭(Generic)은 자바에서 데이터의 타입을 일반화할 수 있는 기능입니다. 제네릭을 사용하면 클래스, 인터페이스, 메소드를 정의할 때 특정 데이터 타입에 의존하지 않고 여러 데이터 타입을 다룰 수 있게 됩니다. 이는 컴파일 시 타입 체크를 가능하게 하여 타입 안정성을 높이고, 형 변환(casting)의 필요성을 줄여 코드의 안전성과 가독성을 향상시킵니다.
1. 제네릭을 사용하는 이유
타입 안정성: 컴파일 시에 데이터 타입을 체크하기 때문에, 실행 중에 발생할 수 있는 ClassCastException을 방지할 수 있습니다.
코드 재사용성: 다양한 타입을 다루는 재사용 가능한 클래스와 메소드를 만들 수 있습니다.
가독성: 코드에서 데이터 타입이 명확히 드러나므로 읽기 쉽고 이해하기 쉬운 코드를 작성할 수 있습니다.
2. 제네릭의 기본 문법
제네릭 타입을 사용할 때는 타입 파라미터를 사용합니다. 가장 흔히 쓰는 타입 파라미터는 <T>로, 이는 임의의 타입을 의미합니다. 자주 사용되는 타입 파라미터는 다음과 같습니다.
T: Type (자료형)
E: Element (컬렉션의 원소)
K: Key (키)
V: Value (값)
N: Number (숫자)
3. 제네릭 클래스
제네릭 클래스 정의
제네릭 클래스를 정의할 때는 클래스 이름 뒤에 <T>와 같은 타입 파라미터를 추가합니다.
edu.ch9.generic 패키지
public class Box<T> {
private T item;
public void setItem(T item) {
this.item = item;
}
public T getItem() {
return item;
}
}
이 예제에서 <T>는 임의의 타입을 나타내며, 이 클래스를 사용할 때 어떤 타입을 사용할지 지정할 수 있습니다.
제네릭 클래스 사용
edu.ch9.generic 패키지
public class Main {
public static void main(String[] args) {
// 제네릭 타입을 Integer로 지정
Box<Integer> integerBox = new Box<>();
integerBox.setItem(123);
System.out.println("Box의 내용: " + integerBox.getItem()); // 출력: Box의 내용: 123
// 제네릭 타입을 String으로 지정
Box<String> stringBox = new Box<>();
stringBox.setItem("Hello");
System.out.println("Box의 내용: " + stringBox.getItem()); // 출력: Box의 내용: Hello
}
}
Box<Integer>와 Box<String>은 제네릭을 통해 다양한 타입을 수용할 수 있게 됩니다.
컴파일 시에 타입이 체크되므로 타입 안정성이 보장됩니다.
4. 제네릭 메소드
제네릭 메소드는 메소드의 리턴 타입 앞에 타입 파라미터를 추가하여 정의합니다. 이렇게 하면 메소드에서 사용되는 매개변수나 리턴 타입을 제네릭으로 지정할 수 있습니다.
제네릭 메소드 정의
제네릭 메소드 간단한 예제
public static <T> T genericTest(T a, T b) {
// 두 값을 출력한 뒤 첫 번째 값을 반환
System.out.println("첫 번째 값: " + a);
System.out.println("두 번째 값: " + b);
return a;
}
public static void main(String[] args) {
// 문자열 타입
String result1 = genericTest("Hello", "World");
System.out.println("반환된 값: " + result1);
// 정수 타입
Integer result2 = genericTest(10, 20);
System.out.println("반환된 값: " + result2);
// 실수 타입
Double result3 = genericTest(3.14, 2.71);
System.out.println("반환된 값: " + result3);
}
edu.ch9.generic 패키지
public class Util {
// 제네릭 메소드
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}
}
제네릭 메소드 사용
edu.ch9.generic 패키지
public class Main {
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
String[] strArray = {"A", "B", "C"};
// 제네릭 메소드 호출
Util.printArray(intArray); // 출력: 1 2 3 4 5
Util.printArray(strArray); // 출력: A B C
}
}
<T>는 메소드가 호출될 때 타입을 지정할 수 있도록 해주며, 이로써 메소드가 다양한 타입을 수용할 수 있습니다.
5. 제네릭 타입 제한 (Bounded Type)
때로는 제네릭 타입에 특정 상위 클래스나 인터페이스를 상속받는 타입만 허용하고 싶을 때가 있습니다. 이럴 때 제네릭 타입 제한을 사용할 수 있습니다.
상위 클래스 제한
edu.ch9.generic 패키지
// Number를 상속하는 타입만 허용
public class Calculator<T extends Number> {
public double add(T num1, T num2) {
return num1.doubleValue() + num2.doubleValue();
}
}
제네릭 타입 제한 사용
edu.ch9.generic 패키지
public class Main {
public static void main(String[] args) {
Calculator<Integer> intCalculator = new Calculator<>();
System.out.println("합계: "
+ intCalculator.add(10, 20)); // 출력: 합계: 30.0
Calculator<Double> doubleCalculator = new Calculator<>();
System.out.println("합계: "
+ doubleCalculator.add(5.5, 3.2)); // 출력: 합계: 8.7
// Calculator<String> stringCalculator = new Calculator<>();
// 오류: String은 Number를 상속하지 않음
}
}
<T extends Number>는 Number를 상속받는 타입만 T로 사용할 수 있도록 제한합니다.
이렇게 하면 제네릭 클래스나 메소드에서 특정 타입 계층구조만 수용할 수 있도록 제한할 수 있습니다.
제네릭 타입 제한 추가 예제
interface Shape {
double getArea();
}
class Circle implements Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double getArea() {
return Math.PI * radius * radius;
}
}
class Rectangle implements Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double getArea() {
return width * height;
}
}
public class ShapePrinter<T extends Shape> {
public void printArea(T shape) {
System.out.println("Shape area: " + shape.getArea());
}
public static void main(String[] args) {
ShapePrinter<Circle> circlePrinter = new ShapePrinter<>();
circlePrinter.printArea(new Circle(5)); // 출력: Shape area: 78.53981633974483
ShapePrinter<Rectangle> rectanglePrinter = new ShapePrinter<>();
rectanglePrinter.printArea(new Rectangle(4, 5)); // 출력: Shape area: 20.0
}
}
특정 인터페이스를 구현한 클래스 만으로 타입을 허용하도록 제한할 수 있습니다.
6. 와일드카드 (?)
와일드카드(?)는 제네릭 타입을 **유연하게 사용(다형성)**하기 위해 사용됩니다.
제한 없는 와일드카드 (?)
모든 타입을 허용
상한 제한 와일드카드 (? extends Type)
특정 클래스의 하위 타입만 허용
하한 제한 와일드카드 (? super Type)
특정 클래스의 상위 타입만 허용
와일드카드 사용 예시
제한 없는 와일드카드 : edu.ch9.generic 패키지
import java.util.ArrayList;
import java.util.List;
public class WildcardExample {
// 모든 타입의 리스트를 출력 (와일드카드 사용)
public static void printList(List<?> list) {
for (Object element : list) {
System.out.print(element + " ");
}
System.out.println();
}
public static void main(String[] args) {
List<Integer> intList = new ArrayList<>();
intList.add(1);
intList.add(2);
intList.add(3);
List<String> strList = new ArrayList<>();
strList.add("A");
strList.add("B");
strList.add("C");
// 와일드카드를 사용하여 어떤 타입의 리스트든 전달 가능
printList(intList); // 출력: 1 2 3
printList(strList); // 출력: A B C
}
}
상한 제한 와일드카드 (? extends Type)
import java.util.List;
public class WildcardExample {
// 상한 제한 와일드카드: Number 또는 그 하위 클래스만 허용
public static double calculateSum(List<? extends Number> list) {
double sum = 0;
for (Number number : list) {
sum += number.doubleValue(); // Number의 doubleValue() 메서드 사용 가능
}
return sum;
}
public static void main(String[] args) {
List<Integer> intList = List.of(1, 2, 3, 4, 5);
List<Double> doubleList = List.of(1.1, 2.2, 3.3);
System.out.println("정수 리스트 합계: " + calculateSum(intList)); // 출력: 15.0
System.out.println("실수 리스트 합계: " + calculateSum(doubleList)); // 출력: 6.6
}
}
하한 제한 와일드카드 (? super Type)
import java.util.List;
import java.util.ArrayList;
public class WildcardExample {
// 하한 제한 와일드카드: Integer 또는 그 상위 클래스만 허용
public static void addNumbers(List<? super Integer> list) {
list.add(10); // Integer 타입 추가 가능
list.add(20);
}
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
List<Object> objectList = new ArrayList<>();
addNumbers(numberList);
addNumbers(objectList);
System.out.println("Number 리스트: " + numberList); // 출력: [10, 20]
System.out.println("Object 리스트: " + objectList); // 출력: [10, 20]
}
}
7. 제네릭의 제한점
기본 자료형 사용 불가: 제네릭에서는 기본 자료형(int, double 등)을 직접 사용할 수 없습니다. 대신, Wrapper 클래스(Integer, Double 등)를 사용해야 합니다.
런타임 시 타입 정보 소실: 제네릭은 컴파일 시에만 타입을 체크하며, 런타임 시에는 타입 정보가 제거되는 타입 소거(type erasure)가 일어납니다. 따라서 런타임에는 제네릭 타입에 대한 정확한 타입 정보를 알 수 없습니다.
8. 제네릭의 장점
타입 안정성: 컴파일 시에 데이터 타입을 체크하여, 런타임 오류를 방지할 수 있습니다.
코드 재사용성: 다양한 데이터 타입을 처리하는 재사용 가능한 클래스 및 메소드를 작성할 수 있습니다.
형 변환의 필요성 감소: 제네릭을 사용하면 명시적 형 변환이 필요하지 않아, 코드가 더 깔끔하고 에러가 줄어듭니다.
정리
제네릭은 자바에서 타입 안정성을 유지하면서 유연하고 재사용 가능한 코드를 작성하는 데 필수적인 기능입니다. 이를 통해 여러 타입의 객체를 하나의 클래스나 메소드로 처리할 수 있으며, 컴파일 시 타입 체크를 통해 안전한 코드를 작성할 수 있습니다.