[자바] Object클래스, Comparable / Comparator, Annotation, Reflection

tech_bae·2025년 3월 16일

Java

목록 보기
5/10
post-thumbnail

Object 클래스

모든 클래스의 최상위 부모 클래스

⇒ 모든 클래스는 Object클래스를 자동으로 상속

class Test /*extends Object 생략 */ {}

Object클래스에는 모든 객체가 사용할 수 있는 기본 메서드 포함

  • equals()
    • 기본은 == 연산자와 같이 메모리 주소를 비교 하지만 override를 통해 논리적비교 구현 가능(값 비교)
  • hashCode()
    • 객체를 해시기반 자료구조에 저장할때 사용
    • 같은 객체라면 동일한 hashCode값을 반환해야함 ⇒ equals() 오버라이딩할때 함꼐 오버라이딩해야 하는 이유
  • toString()
    • 객체의 정보를 문자열로 표현
    • 기본적으로 클래스명@해시코드 형태로 출력
      • 오버라이딩을 통해 커스텀가능

equals() 오버라이딩

  • 동등성 : 객체가 가지고 있는 값을 비교(논리적 비교)
  • 동일성 : 객체의 메모리 주소값을 비교(==, 물리적 비교)

equals()는 기본적으로 == 와 동일하게 객체의 실제 메모리 주소값을 비교

⇒ 같은 데이터값을 가진 객체라도 다르다고 판단

객체간의 동등성을 비교하기 위해선 equals()를 오버라이딩하여 사용해야한다.

public class Person {
    public String name;
    public int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
		    if (this == o) return true; // 같은 객체인지 확인(두 객체의 메모리 주소가 완전히 같은 객체)
        if (o == null || getClass() != o.getClass()) return false;//null이면 false, getClass를 통해 클래스 타입확인
        Person person = (Person) o; //o가 Person타입임이 확인 -> Person타입으로 다운캐스팅
        return age == person.age && Objects.equals(name, person.name);
        }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}
public class Main {
    public static void main(String[] args) {
        Person person1 = new Person("John Doe", 27);
        Person person2 = new Person("John Doe", 27);

        boolean equals = person1.equals(person2);
        System.out.println(equals);
    }
}

출력
Override: false
Override: true

equals()를 오버라이딩하는 과정에서 마지막에 실질적인 비교를 할때

a.equals(b) 를 사용하면 anull 인 경우 NullPointerException 발생 가능

Objects.equals(a, b)null를 자동으로 처리해 안전하게 비교가능

왜 hashCode()를 함께 오버라이딩 해야하는가

  • hashCode()는 객체를 해시기반 컬렉션(HashMap, HashSet …)에 저장하거나 비교할 때 사용
  • equals()만 오버라이딩하면 논리적으로 같은 객체가 다른 해시코드를 가질 수 있음.

⇒ 결과적으로 논리적으로 같은 객체(모든 필드의 값이 같은 객체)를 컬렉션에 중복 저장될 수 있음

문자열 상수

사실 같은 값을 가지고 있는String타입들을 비교하면 == 연산자를 통해서도 true 가 반환된다.

그렇다면 equals() 를 오버라이딩해야하는 이유가 없지 않은가!

자바에서는 문자열(String)을 효율적으로 관리하기 위해서 문자열 상수 풀이라는 메모리 영역을 사용한다.

문자열 상수풀은 Heap영역에 일부로, 동일한 문자열 리터럴을 재사용하여 메모리 낭비를 방지한다.

⇒ 즉 같은 값을 가진 String 객체들은 그 하나의 객체만을 재사용하여 참조한다.

public class Main {
    public static void main(String[] args) {
        String str1 = "Hello";
        String str2 = "Hello";

        boolean areTheySame = str1==str2;
        
        System.out.println(areTheySame); //ture
    }
}

하지만 new 키워드로 String 객체를 생성하면 Heap 영역에 새로운 객체가 생성된다.

⇒ 다른 객체를 참조하게 되어 == 연산자로 비교하면 false 반환

public class Main {
    public static void main(String[] args) {
        String str1 = new String("Hello");
        String str2 = new String("Hello");

        boolean areTheySame = str1==str2;
        boolean theValuesAreSame = str1.equals(str2);
        System.out.println(areTheySame); //false
        System.out.println(theValuesAreSame); //true
    }
}

따라서 이럴 땐 equals()를 사용하여 객체의 값을 비교해야한다.

Comparable / Comparator

Comparable

  • 클래스 자체에서 정렬 기준을 지정(기본 정렬기준 지정)
  • compareTo() 메소드를 오버라이딩하여 정의
  • Collections.sort() , Arrays.sort()를 사용하면 자동으로 정용
public class BodyBuilders implements Comparable<BodyBuilders> {
    protected String name;
    protected int age;
    protected int volume;

    public BodyBuilders(String name, int age, int volume) {
        this.name = name;
        this.age = age;
        this.volume = volume;
    }

    @Override
    public int compareTo(BodyBuilders o) {
        return Integer.compare(this.volume, o.volume);
    }

    @Override
    public String toString() {
        return
                "name='" + name + '\'' +
                ", age=" + age +
                ", volume=" + volume +
                '}';
    }
}
public class Main {
    public static void main(String[] args) {
        List<BodyBuilders> builders = new ArrayList<>();
        builders.add(new BodyBuilders("John", 20, 20));
        builders.add(new BodyBuilders("Tom", 23, 28));
        builders.add(new BodyBuilders("Jack", 28, 30));
        builders.add(new BodyBuilders("Jane", 31, 18));

        System.out.println(builders);
        builders.sort(null); //Collections.sort(builders);
        System.out.println(builders);
    }
}
public int compareTo(BodyBuilders o) {
        return Integer.compare(this.volume, o.volume);
    }

이 부분을 내림차순으로 하고 싶다면

public int compareTo(BodyBuilders o) {
        return Integer.compare(o.volume, this.volume);
    }

이렇게 둘의 순서를 바꿔주면 된다.

왜냐하면

Integer.compare()

public static int compare(int x, int y) {
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}

이렇게 생겨먹었다.

  • o1이 o2 보다 작으면 음수(x와 y의 위치를 유지) : o1 - o2 = 음수
  • 두 값이 같으면 0
  • o1이 o2보다 크면 양수(x와 y의 위치를 바꿈) : o1 - o2 = 양수

그러므로 비교 순서를 바꾸면 내림차순으로 정렬 된다.

이는 Comparatorcompare()에도 동일하게 적용된다.

Comparator

  • 기본 정렬과 다른 정렬기준을 정립
  • compare() 메소드를 오버라이딩하여 정의
  • 클래스 자체의 변경없이 여러 정렬 기준을 만들 수 있음(클래스 외부에서 구현)
public class BuilderComparator implements Comparator<BodyBuilders> {
    @Override
    public int compare(BodyBuilders o1, BodyBuilders o2) {
        return Integer.compare(o1.age,o2.age);
    }
}
public class Main {
    public static void main(String[] args) {
        List<BodyBuilders> builders = new ArrayList<>();
        builders.add(new BodyBuilders("John", 45, 20));
        builders.add(new BodyBuilders("Tom", 23, 28));
        builders.add(new BodyBuilders("Jack", 28, 30));
        builders.add(new BodyBuilders("Jane", 31, 18));

        System.out.println(builders);
        builders.sort(new BuilderComparator()); //Collections.sort(builders, new BuilderComparator);
        System.out.println(builders);
    }
}

다중정렬

builders.sort((o1, o2) -> {
            if(o1.age == o2.age) {
                return Integer.compare(o2.volume, o1.volume);
            }
            return Integer.compare(o1.age, o2.age);
        });

이 방법이 지금의 나에겐 가장 현실적인 방법 같다.

밑의 두 방법은 아직 나에겐 무리다.. 미래를 기약하겠다.

userGuild.sort(
        Comparator.comparing(
                (Character c) -> c.getGold(), Comparator.reverseOrder()
        ).thenComparing(
                (Character c) -> c.getLevel()
        )
);

userGuild.sort(
        Comparator.comparing(
                Character::getGold, Comparator.reverseOrder()
        ).thenComparing(
                Character::getLevel
        )
);

Annotation

  • 컴파일러가 읽을 수 있는 주석이라고 말할 수 있음
  • 자바파일에서 클래스파일로 바뀌어도(컴파일이 끝나도) 유지된다.
  • 일반주석은 사람에게 코드를 설명하는 용도와 비슷하게 Annotation은 프로그램(컴파일러, 런타임)에게 코드를 설명하는 역할을 수행한다.
  • Annotation에 값을 넣어 줄 수 있다.

표준(내장) Annotation

자바가 기본적으로 제공하는 어노테이션이다.

  1. @Override

    오버라이딩을 올바르게 했는지 컴파일러가 체크하게 한다.

  2. @Deprected

    앞으로 사용하지 않는 것을 권장함.

    @Deprecated
    public class TrashClass {
        
        @Deprecated
        public void trash() {
            System.out.println("Trash");
        }
    }

    @Deprecated를 붙은 클래스나 메소드를 사용하려고 하면 밑줄따위가 그어지며 사용하지 않을 것을 권장한다.

  3. @FunctionalInterface

    함수형 인터페이스가 하나의 추상메서드만 가지고 있는지를 확인한다.

    이는 람다을 사용하기 위해서 필요한 개념이다.

  1. @SuppressWarnings(무시할 경고)

    컴파일러의 경고메세지를 무시한다.

    "unchecked", “deprected” , "all" 등이 올 수 있다.

메타 Annotation

어노테이션을 위한 어노테이션

  1. @Target

    어노테이션을 정의할 때, 적용대상을 지정

    @Target({ElementType.TYPE, ElementType.METHOD}) 이런식으로 사용

  2. @Retention

    어노테이션의 유지기간을 지정

    • Runtime 클래스 파일에도 존재, 실행시 사용가능(값 호출 가능)
    • Source 컴파일되면 사라짐 (걍 주석)

    @Rentention(RetentionPolicy.SOURCE) 사용 예

그 외. @Documented, @Inherited, @Repeatable 등..

어노테이션 생성

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetTest {
    String name() default "Test";
    int testCnt();
}

Reflection

  • 리플렉션은 힙 영역에 로드된 Class 타입의 객체를 통해, 원하는 클래스의 인스턴스를 생성
  • 인스턴스의 시그니쳐에 접근 제어자와 상관없이 접근이 가능하도록 하는 API

런타임(실행 시간)에 클래스, 메서드, 필드 등의 정보를 조회하고 조작할 수 있는 기능을 의미합니다.

⇒ 실행 중에 객체의 구조를 분석하고 동적으로 변경

JVM의 클래스 로더에서 클래스 파일에 대한 로딩을 완료 후 해당 클래스의 정보를 담은 Class 타입의 객체를 생성 후 힙 영역에 저장 new키워드로 만드는 객체 와는 다르다.

Class 객체 로드 방법

  1. 클래스 이름으로 Class 객체 가져오기

    Class<?> cl1 = Example.class;

  2. 객체를 통한 Class 객체 가져오기

    Example example = new Example();

    Class<?> cls2 = example.getClass();

  1. 클래스 이름을 문자열로 가져오기

    Class<?> cls3 = Class.forName("Example");

    • 클래스 이름을 문자열로 다룰 수 있기 때문에 동적으로 로딩이 가능.
Example example = new Example();
Class<?> cls2 = example.getClass();

여기서 나는 저 example 객체와 cls2 객체가 다른건가라는 생각을 했다.

결과적으로 다르다.

  • example객체는 Example클래스의 실제 객체
  • cls2객체는 Example의 클래스 메타정보를 가지고 있는 Class<?>의 객체이다.

여기까지 난 특정 클래스의 정보를 가지고 있는Class 객체를 생성?불러와서 이 객체를 통해서 특정 클래스의 정보를 가져올 수 있구나로 이해했다.

근데 왜 이게 되는거지가 궁금해서 좀 찾아보니 다음과 같은 과정으로 설명할 수 있을 거 같다.

  1. A라는 클래스를 생성하면 JVM이 A클래스의 정보를 메소드 영역에 저장한다
  2. Class 객체를 생성하면 Class 객체가 Heap영역에 로드되고, 메모리 영역에 있는 A클래스의 정보를 참조한다.

image.png

이제야 좀 이해가 되는 거 같다. 근데 의문점이 있다. 어째서 Class객체는 메타데이터의 접근제어자를 무시하는가..

⇒ 좀 찾아보니 JVM내부적으로 setAccessible(true)를 사용해서 접근 제한을 해제한단다.. 너무 딥해지니 이제 그만알아보자..

주요 메서드

클래스 관련

  • getName() : 클래스 전체 이름 (패키지 포함)
  • getSimpleName() : 클래스 이름 (패키지 제외)
  • getPackage() : 패키지정보
  • getSuperclass() : 부모클래스 정보
  • getInterfaces() : 클래스가 구현한 인터페이스 정보

생성자 관련

  • Constructor<?>[] constructor = cls.getConstructors(); : public 생성자들을 배열의 형태로 가져옴
  • Constructor<?>[] constructor = cls.getConstructor(String.class); : 특정 매개변수 생성자 가져오기
  • Constructor<?>[] constructor = cls.getDeclaredConstructors(); : private 생성자도 가져
  • Object obj = constructor.newInstance("어쩌고"); : 생성자를 이용해 객체 생성

필드 관련

  • Field[] fields = cls.getFields() : 필드 배열 가져오기

    • Field field = cls.getField("필드명") : 특정 public 필드 가져오기
  • getDeclaredFields() : 모든 필드 가져오기

  • getDeclaredField("필드명") : 특정 private필드 가져오기

  • get(obj) : 객체의 필드 값을 가져옴(인스턴스 → 객체 필요, Static필드 → 객체 불필요)

  • set(obj, "필드이름") : 필드값 변경

메소드 관련

  • invoke(obj) : 매개변수 없는 메서드 실행
  • invoke(obj, "매개변수 명", 인자값) : 매개변수 있는 메서드 실행

어노테이션 관련

  • isAnnotationPresent(MyAnnotation.class) : 클래스의 모든 어노테이션 가져오기
  • getAnnotation(MyAnnotation.class) : 특정 어노테이션 가져오기
profile
전 아무고토 몰루고 아무고토 못해여

0개의 댓글