본문 바로가기
👨‍🏫Study/JAVA

[JAVA] 11 - 1 기본 API 클래스(Object, Objects)

by 코푸는 개발자 2022. 3. 21.
728x90

API란?

  • Application Programming Interface의 약자이다.
  • 컴포넌트들을 결합하기 위한 매개체 인터페이스이다.
  • 호출을 위한 수단이다.
  • 내부 구현을 볼 수 없는 경우도 많지만, 자바 내부의 표준 API들은 내부 구현을 볼 수 있다.

API들은 경로 \jre\lib\rt.jar라는 압축 파일에 저장되어 있다.

자바 API 도큐먼트에서 API의 내용을 확인할 수 있다.
API 클래스의 필드, 생성자, 메소드 등을 확인할 수 있다.

java.lang 패키지

자바 프로그램의 기본적인 클래스를 담고 있는 패키지로 import 없이 사용할 수 있다.

java.lang 대표 내장 클래스

  • Object
    • 자바 클래스의 최상위 클래스로 사용
  • System
    • 표준 입력 장치(키보드)로부터 데이터를 입력받을 때 사용
    • 표준 출력 장치(모니터)로 출력하기 위해 사용
    • 자바 가상 기계를 종료시킬 때 사용
    • 쓰레기 수집기를 실행 요청할 때 사용
  • Class
    • 클래스를 메모리로 로딩할 때 사용
  • String
    • 문자열을 저장하고 여러가지 정보를 얻을 때 사용
  • StringBuffer, StringBuilder
    • 문자열을 저장하고 내부 문자열을 조작할 때 사용
  • Math
    • 수학 함수를 이용할 때 사용
  • Wrapper
    • Byte, Short, Character, Integer, Float, Double, Boolean, Long
    • primitive type을 감싸는(Wrapping) 클래스
    • 기본 타입의 데이터를 갖는 객체를 만들 때 사용
      • primitive 타입에 null이 불가한 특성 등을 해결할 때
      • 편의 메소드를 이용하고 싶을 때
    • 문자열을 기본 타입으로 변환할 때 사용
    • 입력값 검사에 사용

java.util 패키지

주로 컬렉션 클래스들이 있다.

  • Arrays
    • 배열을 조작(비교, 복사, 정렬, 찾기)할 때 사용
  • Calendar
    • 운영체제의 날짜와 시간을 얻을 때 사용
  • Date
    • 날짜와 시간 정보를 저장하는 클래스
  • Objects
    • 객체 비교, 널(null) 여부 등을 조사할 때 사용
  • StringTokenizer
    • 특정 문자로 구분된 문자열을 뽑아낼 때 사용
  • Random
    • 난수를 얻을 때 사용

Object 클래스 (java.lang)

모든 클래스가 기본적으로 상속하는 최상위 부모 클래스이다.

객체 비교 (Object.equals())

public boolean equals(Object obj) { ... }

파라미터가 Object 타입이라 모든 클래스를 전부 파라미터로 받을 수 있다. 또한, Object 클래스는 모든 클래스의 최상위 클래스이기 때문에 모든 클래스는 .equals()메소드를 갖는다.

Object 클래스의 .equals() 메소드는 기본적으로 == 연산자와 동일한 동작을 갖는다. 하지만 .equals() 메소드는 Object에서 기본으로 제공하는 형태로는 거의 쓰이지 않고 메소드의 내용을 오버라이드하여, .equals() 메소드의 동작을 두 객체가 논리적으로 동일한지 판단하는데 쓴다.

이를테면 String 클래스에서는 .equals()가 객체의 주소를 비교하지 않고, 오직 문자열의 내용을 비교하도록 오버라이딩 되어있다.

.equals() 메소드를 오버라이드할 때는 인자로 Object 타입의 객체를 받기 때문에 사실상 어떠한 클래스의 객체도 올 수 있다. 그래서 instanceof 연산자로 해당 객체가 내가 원하는 클래스의 객체인지 확인하는 작업이 가장 먼저 필요하다.

@Override
public boolean equals(Object obj) {
  if(obj instanceof TargetObject) {
    TargetObject targetObject = (TargetObject) obj;
    if(this.id == targetObject.id) {
      return true;
    }
  }
  
  return false;
}

객체 해시코드(hashCode())

  • .hashCode()는 객체 식별에 사용되는 유일한 정수값을 반환한다.
    • 유일하기 때문에, 모든 객체는 다른 정수값을 반환한다.
  • 논리적 동등을 완벽하게 비교하려면 .equals()와 .hashCode()를 둘 다 오버라이드 해야 한다.
  • 컬렉션 프레임워크 HashSet, HashMap, Hashtable는 .hashCode()와 .equals() 두 메소드 모두를 이용하여 논리적 동등을 비교한다.
    • .hashCode()를 수행하고 같은 반환값을 가지는지 먼저 확인하고 .equals()를 통해 다시 한번 같은 반환값을 가지는지 확인한다.

그렇다면 객체 해시코드를 반환하는 .hashCode() 메소드를 오버라이드 해버리면 유일한 값은 어떻게 찾아와야 할까? 그 해답은 바로 System.identityHascode() 메소드를 사용하면 기존의 .hashCode()와 같은 기능을 이용할 수 있다.

HashMap 코드로 살펴보기

Key 클래스

public class Key {
    int id;
    String name;

    public Key(int id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public int hashCode() {
        System.out.println("Key.hashCode of " + name);
        return id;
    }

    @Override
    public boolean equals(Object obj) {
        System.out.println("Key.equals of " + name);
        if(obj instanceof Key) {
            Key argKey = (Key) obj;
            return this.id == argKey.id;
        }

        return false;
    }
}
  • Key라는 클래스를 새로 작성하여 .equals() 메소드와 .hashCode() 메소드를 위와 같이 오버라이딩 하였다.
  • 클래스 멤버 중 id로만 논리적 동치 판단을 하도록 구성하였다.

Main 클래스

public class Main {
    public static void main(String[] args) {
        Key key1 = new Key(1, "firstKey");
        Key key2 = new Key(1, "secondKey");

        HashMap<Key, String> hashMap = new HashMap<>();

        // key1로 홍길동이란 값 삽입
        hashMap.put(key1, "홍길동");

        // key2로 해쉬맵에 있는 일치하는 값 찾기
        String result = hashMap.get(key2);
        System.out.println("result = " + result);
    }
}
  • HashMap 객체를 생성하고, Key의 타입은 위에서 직접 만든 Key 클래스로 하였다.
  • .put() 메소드로 값을 넣을 때 사용한 키는 key1 변수이다.
  • .get() 메소드로 값을 가져올 때 사용한 키는 key2 변수이다.
  • 하지만, 둘의 id 필드의 값은 같고, .equals()와 .hashCode()를 그에 맞게 구현했기에 논리적 동치가 가능하다.

실행 결과

  • .hashCode() 메소드나 .equals() 메소드가 호출되면 콘솔에 출력하도록 프로그래밍 하였다.
    • .hashCode()가 비교를 위해 2번 호출되는 것을 볼 수 있다.
    • .equals()는 한번만 호출되어도 비교가 가능하여 1번 호출된 것을 볼 수 있다.
  • 결과적으로 key1과 key2가 HashMap 내부 구현에 따라 논리적 동치로 판단되고 저장했던 문자열인 홍길동 이 출력된 것을 볼 수 있다.

객체 문자 정보(toString())

  • .toString()메소드는 객체의 문자 정보를 출력한다.
    • 기본 구현은 객체의 주소인 클래스명@16진수해시코드를 리턴한다.
Object obj = new Object();
System.out.println(obj.toString());

// java.lang.Object@de6ced

보통 번지수는 사용자의 관심사가 아니어서 의미없는 정보이기 때문에, 오버라이드하여 사용자의 관심사인 필드의 내용 등을 출력하는 방식으로 많이 고쳐쓴다.

객체 복제(clone())

  • 원본 객체의 필드값과 동일한 값을 가지는 새로운 객체를 생성하는 메소드이다.
    • 원본 객체의 안전을 보장하기 위해 많이 사용된다.
  • 이 메소드를 사용하려면 클래스에서 Cloneable 인터페이스를 구현해야 한다.
    • 구현하지 않으면, 설계자가 복제를 허용하지 않는다는 뜻이다.
      • 구현하지 않은 채로 .clone() 메소드를 쓰면 CloneNotSupportedException 예외가 발생한다.
  • CloneNotSupportedException 예외처리가 필요하기 때문에 try-catch 구문이 필요하다.

getter의 결과로 복제된 클래스를 내보내면, 클래스의 불변성을 지킬 수 있다.

얕은 복제(thin clone)

  • 단순히 필드 값만 복사해서 객체를 복제하는 것을 말한다.
    • 참조 객체를 복사할 때는 동일한 객체를 생성하는 것이 아닌 단순히 주소만 복사한다.

얕은 복제(thin clone) 코드 테스트

CloneableClass 클래스 작성, .getCloneableClass() 메소드 호출 시에 Object에서 상속받은 .clone() 메소드를 그대로 사용하여 반환한다.

public class CloneableClass implements Cloneable {
    Integer id;

    public CloneableClass(Integer id) {
        this.id = id;
    }

    public CloneableClass getCloneableClass() throws CloneNotSupportedException {
        return (CloneableClass) clone();
    }

    public Integer getId() {
        return id;
    }
}
  • 위와 같이 그냥 기본으로 Object에서 상속받은 .clone() 메소드를 사용하면 얕은 복제가 된다.

Main 클래스 작성, System.identityHashCode()로 두 객체가 가지고 있는 id 필드의 Integer 객체가 같은 객체인지 해시코드를 출력해본다.

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        CloneableClass cloneableClass = new CloneableClass(1);
        CloneableClass clonedCloneableClass = cloneableClass.getCloneableClass();

        System.out.println(cloneableClass);
        System.out.println(clonedCloneableClass);

        System.out.println(System.identityHashCode(cloneableClass.getId()));
        System.out.println(System.identityHashCode(clonedCloneableClass.getId()));
    }
}

  • 위는 얕은 복제를 한 객체를 직접 출력해보는 소스와 결과다.
  • CloneableClass의 객체들은 각각 다른 주소를 가지고 있지만, 내부에 있는 Integer 클래스 필드는 같은 객체를 가리키고 있다.
    • Integer 클래스가 각각 다른 객체를 가리키게 만드려면 깊은 복제(deep clone)가 필요하다.

깊은 복제(deep clone)

  • 참조하고 있는 객체도 새로 생성하여 복제한다.
  • 깊은 복제를 원할 때는 .clone() 메소드를 재정의하여 직접 참조 객체를 복사하는 코드를 작성해야 한다.

깊은 복제(deep clone) 코드 테스트

CloneableClass 클래스 재작성

public class CloneableClass implements Cloneable {
    Integer id;

    public CloneableClass(Integer id) {
        this.id = id;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        System.out.println("deep clone");
        return new CloneableClass(new Integer(id));
    }

    public CloneableClass getCloneableClass() throws CloneNotSupportedException {
        return (CloneableClass) this.clone();
    }

    public Integer getId() {
        return id;
    }
}
  • clone() 메소드를 오버라이드하여, 매번 clone() 메소드가 호출될 때마다 Integer 객체를 새롭게 생성하도록 바꾸었다.

Main 클래스 재작성

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        CloneableClass cloneableClass = new CloneableClass(15);
        CloneableClass clonedCloneableClass = cloneableClass.getCloneableClass();

        System.out.println(cloneableClass);
        System.out.println(clonedCloneableClass);

        System.out.println(System.identityHashCode(cloneableClass.getId()));
        System.out.println(System.identityHashCode(clonedCloneableClass.getId()));

        System.out.println(cloneableClass.getId());
        System.out.println(clonedCloneableClass.getId());
    }
}
  • 오버라이드된 clone() 메소드가 정상적으로 새로운 Integer 객체를 만들어내는지 다시한번 System.identityHashCode() 메소드를 통해 테스트

  • 서로 다른 hash 값을 반환하는 것을 확인했다.

객체 소멸자(finalize())

  • 객체가 가비지 콜렉터에 의해 소멸당하기 전에 마지막으로 실행시키는 메소드이다.
  • 객체 소멸 전에 마지막으로 사용했던 자원을 닫고 싶거나 데이터를 저장하고 싶다면 Object의 .finalize()를 재정의할 수 있다.
    • 가비지 콜렉터는 객체가 쓰레기가 된 그 순간에 바로 작동하는 것이 아니다.
      • System.gc() 메소드를 호출하여 가급적 빨리 쓰레기 수집기를 돌릴 수 있다.
    • 가비지 콜렉터는 객체를 순서대로 소멸시키지도 않는다.
      • 그래서 finalize() 메소드가 호출되는 시점 자체도 명확하지 않다.
        • 그래서 사실은 리소스 회수는 그냥 일반 메소드에 명시적으로 호출하는 것이 좋다.

Objects 클래스

대략적 정적 메소드들의 기능

  • 객체 비교
  • 해시코드 생성
  • null 여부
  • 객체 문자열 리턴

정적 메소드 목록

  • int compare(T a, T b, Comparator<T> c): Comparator를 이용하여 두 객체 a와 b를 비교한다.
  • boolean deepEquals(Object a, Object b): 두 객체의 깊은 비교(배열의 항목까지 비교)
  • boolean equals(Object a, Object b): 두 객체의 얕은 비교(주소만 비교)
  • int hash(Object... values): 매개값이 저장된 배열의 해시코드 생성
  • boolean isNull(Object obj): 객체가 null인지 조사
  • boolean nonNull(Object obj): 객체가 null이 아닌지 조사
  • T requireNonNull(T obj): 객체가 null이면 예외 발생
  • T requireNonNull(T obj, String message): 객체가 null인 경우 예외 발생(주어진 예외 메시지 포함)
  • T requireNonNull(T obj, Supplier<String> messageSupplier): 객체가 null인 경우 예외 발생(람다식이 만든 예외 메시지 포함)
  • String toString(Object o): 객체의 .toString() 리턴 값을 리턴
  • String toString(Object o, String nullDefault): 객체의 .toString() 리턴 값 리턴, 첫번째 매개값이 null인 경우, 두번째 매개값 리턴

객체 비교(compare(T a, T b, Comparator<T>c))

Objects.compare(T a, T b, Comparator<T>c) 메소드는 두 객체를 비교자(Comparator)로 비교해서 int 값을 리턴한다. java.util.Comparator<T>는 제네릭 인터페이스 타입으로 두 객체를 비교하는 compare(T a, T b) 메소드 시그니처가 있다.

.compare() 메소드는 int 타입을 리턴하며, 일반적으로 a와 b를 비교하여 a가 b보다 작으면 음수(Minus), 같으면 0(Zero), 크면 양수(Plus)를 리턴하도록 구현 클래스를 만든다.

Wrapper 클래스에는 일반적으로 위와 같이 2가지의 인자를 비교하여 작으면 음수, 같으면 0, 크면 양수를 리턴하는 메소드가 구현되어 있다. Integer.compare()와 같은 형식으로 구현되어 있다.

public class ComparatorTest {
    @Test
    public void integerComparatorTest() {
        Integer a = 10;
        Integer b = 20;

        int compareResult = Integer.compare(a, b);
        System.out.println("compareResult = " + compareResult);
        Assertions.assertThat(compareResult).isEqualTo(-1);

        int compareToResult = a.compareTo(b);
        System.out.println("compareToResult = " + compareToResult);
        Assertions.assertThat(compareResult).isEqualTo(-1);

        int compareImplementationResult = compareImplementation(a, b);
        System.out.println("compareImplementationResult = " + compareImplementationResult);
        Assertions.assertThat(compareImplementationResult).isEqualTo(-1);
    }

    public int compareImplementation(int a, int b) {
        if(a > b) {
            return 1;
        } else if( a == b) {
            return 0;
        }

        return -1;
    }
}

나중에는 위의 .compare() 메소드를 이용하여 컬렉션에 대한 정렬 등을 수행할 수도 있다. 또한 직접 만든 클래스를 정렬하고 싶다면, Comparator<> 혹은 Comparable<> 인터페이스를 상속하여, .compare() 메소드를 오버라이드하면 된다.

Comparable 상속을 이용한 컬렉션 정렬 예제

Student 클래스

public class Student implements Comparable<Student>{
    private int number;
    private String name;

    public void setNumber(int number) {
        this.number = number;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public int compareTo(Student student) {
        if(this.number > student.number) return -1;
        return 0;
        // Integer.compare(this.number, student.number); 와 같다
        // 반대로 정렬하고 싶다면 부호만 반대로 바꾸어주면 된다.
    }

    @Override
    public String toString() {
        return "Student{" +
                "number=" + number +
                ", name='" + name + '\'' +
                '}';
    }
}

StudentTest 클래스

public class StudentTest {
    @Test
    public void studentSort() {
        ArrayList<Student> studentArrayList = new ArrayList<>();

        Student student2 = new Student();
        student2.setName("김이번");
        student2.setNumber(2);

        studentArrayList.add(student2);

        Student student1 = new Student();
        student1.setName("김일번");
        student1.setNumber(1);

        studentArrayList.add(student1);

        Student student3 = new Student();
        student3.setName("김삼번");
        student3.setNumber(3);

        studentArrayList.add(student3);

        Collections.sort(studentArrayList);

        System.out.println("studentArrayList = " + studentArrayList);
    }
}

나는 컬렉션의 .sort()에 대해 간단히 comapre 메소드가 음수일 때만 변화를 준다고 이해하고 있다. 왜냐하면 음수 외에 다른 어떤 값을 주어도 변화가 일어나지 않기 때문이다. 무조건 음수만 반환하게 return -1;을 하면 순서가 반대로된 리스트가 반환된다.

 

 

728x90

댓글