이전 글과 이어집니다.

Final Method

  • 우리는 메서드를 final 메서드로 선언할 수 있다. 이렇게 선언하면 어떤 subclass도 이 메서드를 오버라이드 하지 못한다.

다음 예시를 보자.

class Employee {
    private String name;
    private int salary;

    public Employee(String name, int salary) {
        this.name = name;
        this.salary = salary;
    }

    public final String getName() {
        return this.name;
    } // subclass cannot override this method.

    public int getSalary() {
        return this.salary;
    }
}

class Manager extends Employee {
    private int bonus;

    public Manager(String name, int salary) {
        super(name, salary);
        bonus = 10000;
    }

    public void setBonus(int bonus) {
        this.bonus = bonus;
    }

    public String getName() {
        return "Sir " + super.getName();
    } // Error: overriding a final method
}
  • getName 메서드는 final 메서드이기 때문에 Manager 클래스에서 사용될 수 없다.

메서드뿐 아니라 final 클래스도 만들 수 있는데, 메서드와 비슷하게 어떤 subclass도 이 클래스를 상속할 수 없다.

Abstract method and abstract class

클래스 정의에서, 구현 없이 메서드를 선언하는 게 가능하다. 이를 abstract 메서드라고 부르며, 메서드 헤더에 abstract이라는 키워드를 붙여 주어야 한다.

  • 만약 클래스 정의에 abstract 메서드를 포함한다면, 그 클래스는 abstract 클래스이다. 클래스에도 abstract 키워드를 붙여 주어야 한다.

다음 예시를 보자.

abstract class Person {
    private String name;

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

    public final String getName() {
        return name;
    }

    public abstract int getId();
}

이 클래스는 abstract 클래스이다.
Person 클래스를 상속한 subclass는

  • getId() 메서드를 구현하거나
  • 그 자신이 abstract 메서드가 되어야

한다.

abstract 클래스는 interface와 상당히 유사해 보인다. 하지만 그 둘은 다음과 같은 차이점이 있다.

  • abstract 클래스는 인스턴스 변수와 생성자를 가질 수 있다. interface는 불가능하다.

Abstract class

abstract 클래스의 인스턴스를 생성할 수 없다.

Person p = new Person("Tom"); // Error: cannot create instances of an abstract class.

하지만 abstract 클래스를 상속하고 모든 abstract 메서드를 구현한 subclass의 인스턴스는 만들 수 있다.

class Student extends Person {
    private int id;

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

    public int getId() {
        return id;
    }
}
Student s = new Student("Tom", 0303);
System.out.println(s.getName() + " " + s.getId();

또, subclass의 객체를 abstract 클래스인 superclass에 할당할 수도 있다.

Protected access

protected 접근 제어자를 사용해서 변수가 메서드를 선언하면, 그 변수나 메서드는 subclass에서 접근 가능하다.

class Employee {
    private String name;
    protected int salary;

    public Employee(String name, int salary) {
        this.name = name;
        this.salary = salary;
    }

    public String getName() {
        return this.name;
    }

    public int getSalary() {
        return this.salary;
    }
}

class Manager extends Employee {
    private int bonus;

    public Manager(String name, int salary) {
        super(name, salary);
        bonus = 10000;
    }

    public void setBonus(int bonus) {
        this.bonus = bonus;
    }

    public int getSalary() {
        return salary + bonus;
    } // can access superclass protected variable
}
  • 또, protected 변수들은 같은 패키지의 다른 클래스들에서도 접근 가능하다.
    • 다른 패키지의, subclass가 아닌 클래스들은 접근할 수 없다.
    • 다른 패키지에 있는 subclass는 변수와 메서드 둘 다 접근 가능하다.

Superclass and Interface

자바는 다양한 종류의 구현/상속이 가능하다.

  • 클래스는 interface를 구현할 수 있다.

    class Student implements Named {
    ...
    }
  • 클래스는 여러 interface를 구현할 수 있다.

    class Student implements Named, Registered {
    ...
    }
  • 클래스는 superclass를 확장할 수 있다.

    class Student extends Person {
    ...
    }
  • 클래스는 여러 클래스를 확장할 수 없다.

    class Student extends Person, Animal { // Error: cannot extent multiple classes
    }
  • 클래스는 superclass를 확장하고 interface를 구현할 수 있다.

interface Name {
    default String getName() {
        return "NoName";
    }
}

abstract class Person {
    private String name;

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

    public String getName() {
        return name;
    }

    public abstract int getId();
}

class Student extends Person implements Name {
    private int id;

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

    public int getId() {
        return id;
    }
}

만약 다음과 같은 클래스와 interface가 있다고 해 보자. 아래 코드를 실행하면 어떻게 될까?

Student s = new Student("Fred", 1729);    System.out.println(s.getName()); // prints "Fred"

한 클래스가 superclass를 확장하고 interface를 구현했을 때, superclass와 interface에 같은 이름과 매개변수를 가진 메서드가 존재하면

  • superclass가 우선순위를 가진다.
  • getName() 메서드가 호출 되었을 때 superclass에 있는 메서드가 호출된다.

class Object: superclass of all classes

이제, 조금 독특한 클래스에 대해 알아본다.
자바에서 모든 클래스는 암시적으로 Object 클래스를 상속한다.

  • 만약 우리의 클래스가 아무런 클래스도 확장하지 않았을지라도, 그것은 Object 클래스를 암시적으로 확장한다.
  • 아래의 두 클래스는 같은 것이다.
class Student {
  ...
  }
class Student extends Object {
  ...
  }

Object 클래스에는 몇 가지의 메서드들이 정의되어 있다. (후술할 메서드는 그중 일부이다)

  • String toString()
    • 객체의 문자열 표현을 반환한다. (e.g. "java.lang.Object@3c237495)
  • boolean equals(Object other)
    • 객체가 other 객체와 같다면 true를 반환한다.
    • 그렇지 않거나, other 객체가 null인 경우 false를 반환한다.
  • int hashCode()
    • 객체에 할당된 해시 코드를 반환한다.
    • 두 객체가 같으면 같은 해시 코드를 가진다.
  • Class<?> getClass()
    • 객체의 클래스를 표현하는 클래스 객체를 반환한다.
  • protected Object clone()
    • 객체의 복사본을 반환한다.
  • protected void finalize()
    • gerbage collector가 객체를 다시 살릴 때 불리는 메서드이다.

class Object: method toString

객체의 문자열 표현식을 반환한다.

class Employee {
    private String name;
    protected int salary;

    public Employee(String name, int salary) {
        this.name = name;
        this.salary = salary;
    }

    public String getName() {
        return this.name;
    }

    public int getSalary() {
        return this.salary;
    }
}
Employee empl = new Employee("John", 50000);
System.out.println(empl.getName() + " " + empl.getSalary());
System.out.println(empl.toString()); // prints internal representation of emp1

객체를 출력하려고 하면 자동적으로 객체의 toString 메서드가 호출된다.

System.out.println(emp1);

자신의 클래스에서 이 메서드를 오버라이드할 수 있다.

class Employee {
    private String name;
    protected int salary;

    public Employee(String name, int salary) {
        this.name = name;
        this.salary = salary;
    }

    public String toString() {
        return getClass().getName() + "[name=" + name + ",salary=" + salary + "]";
    }

    public String getName() {
        return this.name;
    }

    public int getSalary() {
        return this.salary;
    }
}

만약 위의 클래스를 상속한 어떤 클래스가 있다면, toString 메서드는 그 클래스에서도 마찬가지 양식으로 호출된다.

class Object: method equals

equal 메서드는 두 객체가 같은지 확인한다.

  • 두 객체가 같은 객체면 true를 반환한다.
  • 두 객체가 다르거나 비교되는 객체가 null이면 false를 반환한다.

아래의 코드는 false를 반환한다. 왜냐하면 두 객체는 다르기 때문이다.

Employee empl1 = new Employee("John", 50000);
Employee empl2 = new Employee("John", 50000);
System.out.println(empl1.equals(empl2));

equals 메서드는 두 객체의 "같음"을 비교하기 위해 자주 오버라이드된다.

  • 같음의 정의는 클래스 디자이너에게 달렸다.
  • 예를 들어, String 클래스는 equals 메서드를 두 String 객체의 내용을 비교하는 것으로 오버라이드한다.
  • 다음의 코드는 true를 반환한다.
    String s1 = new String("hello");
    String s2 = new String("hello");
    System.out.println(s1.eqauls(s2));

우리가 직접 작성한 클래스도 equals 메서드를 오버라이드할 수 있다.

class Employee {
    private String name;
    protected int salary;

    public Employee(String name, int salary) {
        this.name = name;
        this.salary = salary;
    }

    public String toString() {
        return getClass().getName() + "[name=" + name + ",salary=" + salary + "]";
    }

    public boolean equals(Object otherObject) {
        if (this == otherObject)
            return true;
        if (otherObject == null)
            return false;
        if (getClass() != otherObject.getClass())
            return false;
        Employee other = (Employee) otherObject;
        return (this.name == other.name && this.salary == other.salary);
    }

    public String getName() {
        return this.name;
    }

    public int getSalary() {
        return this.salary;
    }
}

그런데 equals 메서드를 오버라이드할 때 주의할 점들이 있다.

  • 두 객체가 같은지 확인하고 그렇다면 true를 반환한다.
  • 그런 다음 other 객체가 null인지 확인하고 그렇다면 false를 반환한다.
  • 그런 다음 비교된 두 객체가 같은 클래스인지 확인한다. 다른 클래스라면 false를 반환한다.
    • 다른 클래스들 사이에서도 작동하는 equals 메서드를 구현할 수 있지만, 일반적으로 잘 사용되지 않는다.
  • other 객체를 그 클래스로 변환한다.
  • 인스턴스 변수들들 비교한다. 각각의 인스턴스 변수에서 "=="가 의미하는 바를 주의한다.
    • 때때로 두 인스턴스 변수를 비교하기 위해 equals()를 사용할 수도 있다.

class Object: method hashCode

해시 코드(hashCode)는 객체로부터 계산되는 정수이다.

  • 자바에는 두 객체가 다른 객체라면 아주 높은 확률로 다른 해시코드를 가지는 해시 함수가 정의되어 있다.

아직 우리가 equals()를 오버라이드하지 않았다고 가정해 보자.

class Employee {
    private String name;
    protected int salary;

    public Employee(String name, int salary) {
        this.name = name;
        this.salary = salary;
    }

    public String toString() {
        return getClass().getName() + "[name=" + name + ",salary=" + salary + "]";
    }

    public String getName() {
        return this.name;
    }

    public int getSalary() {
        return this.salary;
    }
}

아래의 코드에서 empl1과 empl2는 다른 객체이기에, 다른 해시 코드를 가진다.

Employee empl1 = new Employee("John", 50000);
Employee empl2 = new Employee("John", 50000);
System.out.println(empl1.hashCode() + " " + empl2.hashCode());

만약 equals 메서드를 오버라이드하면, 같음의 정의에 부합하게 hashCode 메서드도 오버라이드해야 한다.
예를 들어, String 클래스는 equals 메서드와 hashCode 메서드 모두를 오버라이드해서 두 String 객체가 같으면 그들의 해시코드도 같다.

String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1.equals(s2));
System.out.println(s1.hashCode() + " " + s2.hashCode());