코어자바 4장

코어자바 상속과 리플렉션

클래스 확장

슈퍼 클래스와 서브 클래스

extends 키워드를 사용하면 기존 클래스에서 파생된 새 클래스를 만든다는 것을 나타낸다. 이때 기존 클래스를 SuperClass, 새 클래스를 SubClass 라고 한다.

슈퍼 클래스가 서브 클래스보다 상위이지만, 더 우월하지 않다는 점은 유의한다. 오히려 그 반대다. Manager extends Employee 를 보면 슈퍼클래스가 서브 클래스를 포함한다.

메서드 오버라이딩

서브 클래스에서 슈퍼클래스 메서드를 수정할 때도 있다. 서브 클래스 메서드는 슈퍼클래스의 비공개 인스턴스 변수에 직접 접근할 수 없고, 슈퍼 클래스 메서드가 필요할 경우 super 키워드를 사용한다.

super는 this와 달리 객체 참조가 아니며 동적 조회를 우회해 특정 메서드를 호출할 때 사용하는 지시자(directive)이다.

메서드를 오버라이드할 때는 매개변수 타입이 정확하게 일치해야한다.

슈퍼 클래스에 있는 메서드와 타입이 다를 경우 완전히 새로운 메서드이며 @Override 에너테이션을 붙혀 막을 수 있다.

@Override public boolean worksFor(Employee supervisor)

메서드를 오버라이드할 때 반환 타입을 서브타입(하위 타입)으로 바꿀 수 있다.(기술적인 용어로 공변 반환 타입(covariant return type)이라고 한다.)

public class Employee {
    public Employee getSuperVisor() {
    }
}

public class Manager extends Employee {
    @override
    public Manager getSuperVisor() {
    }
}

서브클래스 생성

슈퍼 클래스의 비공개 인스턴스 변수에 접근할 수 없으므로 이런 인스턴스 변수는 슈퍼클래스 생성자로 초기화해야 한다.

public class Employee {
    private int id;

    public Employee(int id) {
        this.id = id;
    }

}

public class Manager extends Employee {
   private String name;

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

서브클래스 생성자에서 슈퍼클래스 생성자를 호출할 때는 첫 번째 문장으로 사용해야한다.

서브 클래스에서 슈퍼 클래스 생성자를 명시적으로 호출하지 않을 때는 슈퍼클래스 안에 암시적으로 호출될 인수 없는 생성자가 있어야 한다.

슈퍼클래스 할당

서브클래스의 객체를 슈퍼 클래스 타입 변수에 할당할 수 있다. 밑의 코드의 결과물은 Manager의 getSalary를 실행한다. 메서드를 호출할 때 가상 머신은 객체의 실제 클래스를 살펴보고, 해당 클래스에 맞는 메서드 버전을 찾아서 실행하는데, 이 과정을 동적 메서드 조회(dynamic method lookup)라고 한다.

public class Employee {
    private double salary;

    public void raiseSalary(double intVal) {
        this.salary += intVal;
    }
}

public class Manager extends Employee {
    private double bonus;

    @Override
        public double getSalary() {
            return super.getSalary() + this.bonus;
        }

}
Manager boss = new Manager();
Employoee empl = boss;
double salary = empl.getSalary();

cast

cast는 형 변환이다. 서브 클래스에서 슈퍼클래스로 casting 하게 되면, 슈퍼클래스에 존재하는 메서드만 호출할 수 있다.

최종 메서드와 최종 클래스

메서드를 final로 선언하면 어느 서브클래스도 해당 메서드를 오버라이드할 수 없다.

public class Employee {
    public final String getName() {
        return name;
    }
}

가끔 자신이 만든 클래스의 서브 클래스를 만들지 못하게 하고 싶을 수도 있는데, 클래스를 정의할 때 final 제어자를 사용해야 한다.

public final class Executive extends Manager

추상 메서드와 추상 클래스

클래스는 구현이 없는 메서드를 선언해 서브 클래스가 해당 메서드를 구현하도록 강제할 수 있다. 이렇게 구현이 없는 메서드를 추상 메서드라고 하며, 추상 메서드가 포함된 클래스를 추상 클래스라고 한다. 추상 메서드와 추상 클래스에는 abstract 제어자를 붙여야 한다. 추상 클래스는 비추상 메서드를 포함할 수 있다.

public abstract class Person {
    private String name;

    public final String getName() { return name; }

    public abstract int getId();
}

추상 클래스의 생성자로 인스턴스를 생성할 수 없다. 하지만 subclass에서 구현하고 인스턴스를 만들때, super의 field를 초기화 할때 사용된다.

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

보호 접근

서브 클래스 전용으로 제한하거나 서브클래스 메서드에서 슈퍼클래스의 인스턴스 변수에 접근하고 싶을 경우, 클래스의 해당 기능을 protected로 선언하면 된다. protected는 패키지 수준 접근 권한을 부여하므로 같은 패키지 내에 있다면 누구나 접근이 가능하며, 다른 패키지에서는 subclass만 접근이 가능하다.

익명 클래스

인터페이스를 구현하는 익명 클래스를 만들 수 있는 것처럼 슈퍼클래스를 확장하는 익명 클래스도 만들 수 있다.

슈퍼클래스 이름 뒤에 오는 괄호 안 인수는 슈퍼클래스 생성자에 전달되며, ArrayList<String>의 익명 서브클래스를 생성하면서 add메서드를 오버라이드 했다.

ArrayList<String> names = new ArrayList<String>(100) {
    public void add(int index, String element) {
        super.add(index, element);
        System.out.println("Adding %s at %d\n", element, index);
    }
};

이중 중괄호 초기화(double brace initialization)는 중괄호가 두 개이며, 바깥쪽 중괄호는 ArrayList<String>의 익명 서브클래스를 만든다. 안쪽 중괄호는 초기화 블록이다.

하지만 권장되는 방법은 아니다.

new ArrayList<String>() {
            @Override
            public void add(int index, String element) {
                super.add(index, element);
            }

            {
                add("Harry");
                add("Sally");
            }
        }

상속과 기본 메서드

클래스를 확장하고 인터페이스를 구현하는 클래스가 있는데, 클래스아 인터페이스에 있는 메서드 이름이 같은 경우 항상 슈퍼클래스 구현이 인터페이스 구현보다 우선한다. 밑의 코드는

public interface Named {
    default String getName() { return "";}
}

public abstract class Person {
    private String name;

    public final String getName() { return name; }
}

public class Student extends Person implements Named{
    private int id;

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

    @Override
    public int getId() {
        return this.id;
    }
}

Student student = new Student("harry", 1);
        System.out.println(student.getName());

super를 이용한 메서드 표현식

object::instanceMethod 형태로 메서드 표현식을 사용할 수 있다. 이때 객체 참조 대신 super를 사용할 수도 있다. 다음은 this를 기준으로 주어진 메서드의 슈퍼클래스 버전을 호출한다.

public class Worker {
    public void work() {
        for (int i = 0; i < 100; i++) System.out.println("Working");

    }
}
public class ConcurrentWorker extends Worker {
    public void work() {
        Thread t = new Thread(super::work);
        t.start();

    }
}

Object: 보편적 슈퍼클래스

자바에서 모든 클래스는 직간접적으로 Object클래스를 확장한다. 클래스에 명시적인 슈퍼클래스가 없으면 암시적으로 Object를 확장한다.

밑의 두 코드는 동일하다.

public class Employee{}

public class Employee extends Object {}

toString

객체의 문자열 표현을 반환하는 toString 메서드는 Object 클래스에서 중요한 메소드중 하나이다.

toString 메서드는 주로 클래스 이름 뒤에 인스턴스 변수 목록을 대괄호([])로 감싸서 나열하는 형식을 따른다.

서브클래스에서는 super.toString()을 호출한 후 해당 서브클래스의 인스턴스 변수를 별도의 대괄호 쌍 안에 넣어 추가한다.

public class Employee implements Cloneable{
    protected double salary;
    private Manager manager;
    private String name;

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

public class Manager extends Employee{
    private double bonus;
    private Manager manager;

public String toString() {
    return super().toString() + "[bonus="+ bonus+"]";
}
}

객체를 문자열과 연결하면 자바 컴파일러는 해당 객체의 toString 메서드를 자동으로 호출한다.

System.out.println(manager+"");

Object 클래스에 정의된 toString 메서드는 클래스 이름과 해시 코드를 출력한다. java.io.PrintStream@2f123 처럼. 이는 PrintStream 클래스를 구현한 사람이 toString 메서드를 오버라이드 하지 않았기 때문이다.

배열을 toString 하면 접두어에 [I가 나오는데, 이는 정수의 배열을 나타낸다. 배열을 toString 하는 방법으로 Arrays.toString()메서드가 있다.

equals 메서드

equals 메서드는 한 객체가 다른 객체와 동등한지 검사한다. Object에 구현된 equals 메서드는 두 객체 참조가 동일한지 판단한다. 오버라이드를 통해 동등을 확인할 수 있다.

밑의 코드는 오버라이드 하여 두 객체가 같은 내용을 담고 있는지 확인하는 equals를 만들고 있다.

public class Item {
    private String description;
    private double price;

    public Item(String description, double price) {
        this.description = description;
        this.price = price;
    }

    @Override
    public boolean equals(Object obj) {
        //두 객체가 동일한지 알아보는 빠른 검사. 참조가 같은지 검사.
        if (this == obj) return true;

        //매개변수가 null이면 false를 반환해야 한다.
        if(obj == null) return false;
        //otherObject가 Item의 인스턴스인지 검사한다.
        if (getClass() != obj.getClass()) return false;
        // 인스턴스 변수들의 값이 동일한지 검사한다.
        Item other = (Item) obj;
        Double a = other.price;
        Double b = price;

        return Objects.equals(description, ((Item) obj).description)
                && a.equals(b);
    }
}

인스턴스 변수가 배열이라면 정적 메서드 Arrays.equals로 배열의 길기가 같고, 대응하는 배열 요소도 같은지 검사하면 된다.

서브클래스에서 equals 메서드를 정의할 때는 먼저 슈퍼 클래스의 equals를 호출하고, 이후에 서브 클래스의 인스턴스 변수를 비교한다.

public class DiscountedItem extends Item {
    private double discount;

    public DiscountedItem(String description, double price, double discount) {
        super(description, price);
        this.discount = discount;
    }

    @Override
    public boolean equals(Object obj) {
        if (!super.equals(obj)) return false;
        DiscountedItem other = (DiscountedItem) obj;
        return other.discount == this.discount;
    }
}

if (getClass() != obj.getClass()) return false 코드 부분을 if(!(obj instanceof Item)) return false;로 대체할 수 있다. 위와 같이 할 경우, 자신의 서브클래스와도 비교를 할수 있다. 하지만, 대칭적이지 않다. 예를들어, item과 discountedItem(할인된 아이템)을 비교할 경우, 두 메서드는 동일한 메서드에서 다른 반환값을 주므로 문제가 된다.

instanceof검사가 적절한 상황으로 동등성 개념이 슈퍼클래스에 고정되어 있으면서 서브클래스에서 절대 변하지 않을대다.

public class Employee {
   private int id;

   public final boolean equals(Object obj) {
       if (this == obj) return true;
       if((!obj instanceof Employee)) return false;
       EMployee other = (Employee) obj;
       return id == other.id;
   }
}

HashCode 메서드

hash code는 객체에서 파생한 정수 값이다. 해시코드는 중복되지 않게 잘 뒤섞여 있어야 한다. x,y가 동등하지 않은 각각의 객체라면 두개의 해쉬값이 다를 확률이 높다.

밑의 코드는 String 클래스가 해시코드를 계산하는 알고리즘이다.

int hash = 0;
for (int i = 0; i < length(); i++)
    hash = 31 * hash + chatAt(i);

hashCode와 equals 메서드는 반드시 호환되어야 한다. 예를들어 x.equals(y)이면 x.hashCode() == y.hashCode()여야 한다.

equals 메서드를 재정의한다면 hashCode 메서드도 재 정의해서 equals와 호환되게 해야한다. 그렇지 않으면 클래스의 사용자가 해시 집합이나 해시맵에 객체를 넣으면 잃을 수 있다.

가변 인수 메서드 Objects.hash는 인수들의 해시 코드를 계산해 결합한다. Objects.hash 메서드는 null에 안전하다.

class Item {
    public int hashCode() {
        return Objects.hash(description, price);
    }
}

클래스에 배열 인스턴스 변수가 있으면 먼저 정적 메서드 Arrays.hashCode로 해당 배열의 해시 코드를 계산한다(Arrays.hashCode는 배열의 요소의 해시코드를 결합해 해시코드를 계산하는 메서드다.) 그런 다음 결과를 Objects.hash에 전달한다.

객체 복제

clone 메서드는 오버라이드하기 복잡하고 하는 경우가 드물다. 그러므로 마땅한 이유가 없으면 override를 안하는게 좋다. clone 메서드의 목적은 객체의 복제본(원본과 상태가 같은 별개의 객체)을 만드는 것이다. 두 객체 중 하나의 상태를 변경하더라도 나머지 하나는 변하지 않는다.

clone 메서드는 Object 클래스에 protected로 선언해서 클래스 사용자가 인스턴스를 복제할 수 있게 하기 위해선 clone 메서드를 오버라이드 해야한다.

Object.clone 메서드는 얕은 복사(shallow copy)를 수행한다. 원본 객체에 있는 모든 인스턴스 변수를 복제된 객체로 단순히 복사한다. 그러므로 ArrayList가 존재하면 복제본이 ArrayList를 공유하므로 문제가 될 수 있다. 그러므로 deep copy를 해야한다.

클래스를 구현할 때 상황

  1. clone메서드를 구현 하지 않아도 되는가 ?

    • 이 경우엔 아무것도 하지 않아도 된다. 구현 클래스가 superclass(object)에서 상속을 받더라도 superclass는 package에 있기 때문에 접근하지 못한다.
  2. 구현한다면 상속 받은 clone 메서드를 사용해도 되는가 ?

    • cloneable 인터페이스를 구현해야한다. cloneable 처럼 아무것도 없는 인터페이스를 marker 또는 tagging 인터페이스라고 한다.
    • 방법

      1. object.clone 메서드는 얕은 복사를 수행하기 위해 cloneable 구현 검사 후 구현 안됬다면 CloneNotSupportedException을 던진다.
      2. clone 메서드의 접근 범위를 public으로 반환 타입을 변경한다.
      3. 마지막으로 위의 예외를 처리해준다. CloneNotSupportedException은 검사예외 이므로 선언하거나 잡아야 한다. 구현중인 클래스가 final이면 예외를 잡고, 아니면 subclass에서 이 예외를 다시 던질수도 있으니 예외를 선언하는게 낫다.
  3. clone 메서드에 깊은 복사를 해야할 경우

    • object.clone 메서드를 쓰지 않는 방법과 쓰는 방법이 잇다.
    • 객체를 복사한 후 객체 내의 객체 참조를 갖는 변수들도 복사 해줘야한다.

상속 받은 clone 메서드 사용하는 코드

public class Employee implements Cloneable{
        @Override
    public Employee clone() throws CloneNotSupportedException {
        return (Employee) super.clone();
    }
}

깊은 복사를 해야하는 경우

public Message clone() {
    Message cloned = new Message(sender, text);
    cloned.recipients = new ArrayList<>(recipients);
    return cloned;
}

@Override
    protected Message clone() {
        //ArrayList 요소들의 참조 들도 모두 복사 해주었음.
        try {
            Message cloned = (Message) super.clone();
            ArrayList<String> clonedRecipients = (ArrayList<String>) recipients.clone();
            cloned.recipients = clonedRecipients;
            return cloned;
        } catch (CloneNotSupportedException e) {
            System.out.println(e);
            return null;
        }
    }

열거

다음은 정확히 인스턴스 네개로 구성된 타입을 정의하는 전형적인 예다.

public enum Size {SMALL, MEDIUM, LARGE, EXTRA_LARGE};

열거의 메서드

열거 타입의 각 인스턴스는 equals 메서드 사용할 필요 없이 ==를 사용하면 된다.

toString을 구현하지 않아도 열거 객체의 이름을 돌려주는 toString 메서드를 자동으로 제공한다.

toString의 역으로 각 열거 타입에 맞게 만들어지는 정적 메서드 valueOf이다.

Size notMySize = Size.valueOf("SMALL");

valueOf 메서드는 지정한 이름에 해당하는 인스턴스가 없으면 예외를 던진다.

각 열거 타입에는 정적 메서드 values가 있고 이는 모든 인스턴스를 선언한 순으로 정렬한 배열을 반환한다.

ordinal 메서드는 선언에서 인스턴스의 위치를 돌려준다. Size.MEDIUM.ordinal() 은 1을 반환

생성자, 메서드, 필드

원한다면 열거 타입에 생성자, 메서드, 필드를 추가할 수 있다. 열거의 생성자는 언제나 비공개이며, private제어자를 생략해도 된다. 하지만 public이나 protected로 선언하면 오류난다.

    public enum Size1 {
        SMALL("S"), MEDIUM("M"), LARGE("L"), EXTRA_LARGE("XL");
        private String abbreviation;

        Size1(String abbreviation) {
            this.abbreviation = abbreviation;
        }

        public String getAbbreviation() {
            return abbreviation;
        }
    }

인스턴스의 바디

enum 인스턴스 각각에 메서드를 추가할 수 있지만, 열거에 정의된 메서드를 오버라이드하는 것이어야 한다.

public enum Operation {

        ADD {
            public int eval(int arg1, int arg2) { return arg1 + arg2;}
        },
        SUBTRACT {
            @Override
            public int eval(int arg1, int arg2) {
                return arg1-arg2;
            }
        },
        MULTIPLY {
            @Override
            public int eval(int arg1, int arg2) {
                return arg1-arg2;
            }
        },
        DIVIDE {
            @Override
            public int eval(int arg1, int arg2) {
                return arg1-arg2;
            }
        };

        public abstract int eval(int arg1, int arg2);
    }

정적 멤버

열거 상수(인스턴스)가 정적 멤버보다 먼저 생성되므로 열거 생성자에서 정적 멤버를 참조할 수 없다. 생성자가 불린 이후에 정적 멤버가 생성된다.

클래스 내부에 열거 타입을 중첩할 수도 있다. 이렇게 중첩된 열거는 암시적으로 정적 중첩 클래스가 된다.

열거를 기준으로 스위치

public static int eval(Operation op, int arg1, int arg2) {
        int result = 0;
        switch (op) {
            case ADD : result = arg1 + arg2; break;
            case SUBTRACT : result = arg1 - arg2; break;
            case MULTIPLY : result = arg1 * arg2; break;
            case DIVIDE : result = arg1 / arg2; break;
        }
        return result;
    }

실행 시간 타입 정보와 리소스

자바는 실행 시간에 객체가 어느 클래스에 속하는지 알아 낼 수 있다. 게다가 클래스를 어떻게 로드했는지 알아내서 클래스와 관련된 데이터, 즉 resource를 로드할 수도 있다.

Class 클래스

객체의 정보(객체가 속한 클래스 같은 정보)를 갖는 클래스이다. Class 객체를 얻고 나면 클래스 이름을 알아낼 수 있다. Class.forName으로 객체를 얻는 방법도 있다. 해당 클래스 또는 인터페이스에 대한 정보를 갖는 class 객체 인스턴스를 생성한다.

Object obj = ...;
Class<?> cl = obj.getClass();

cl.getName();

Class<?> clas = Class.forName("className");

Class.forName 메서드의 용도는 컴파일 시간에 알려지지 않은 클래스의 class 객체를 생성한다. 이것은 Runtime에 입력받은 클래스명을 동적으로 로딩한다. 원하는 클래스를 미리 알고 있다면 아래와 같이 리터럴을 사용할 수 있다.

Class<?> cl = java.util.Scanner.class

다른 타입 정보를 얻을 때도 .class 접미어를 사용할 수 있다.

        Class<?> class2 = double[].class;   //String[] 배열 타입을 기술
        Class<?> class3 = Runnable.class;   //Runnable 인터페이스를 기술
        Class<?> class4 = int.class;        // int 타입을 기술
        Class<?> class5 = void.class;       //void 타입을 기술.

자바에서 배열은 클래스지만 인터페이스, 기본타입, void는 클래스가 아니다. type이 더 정확한 표현이다.

가상머신은 각 타입별로 고유한 Class 객체를 관리한다. 그러므로 == 연산자로 클래스 객체를 비교할 수 있다.

if (other.getClass() == Employee.class)

리소스 로드

Class가 제공하는 유용한 서비스 중 하나는 설정 파일이나 이미지 처럼 프로그램에 필요한 리소스를 찾아오는 것이다. 클래스 파일이 있는 곳과 같은 디렉터리에 리소스를 넣었다면 파일명.확장자로 다음과 같이 입력 스트림을 열 수 있다.

       InputStream stream = Main.class.getResourceAsStream("config.txt");
       Scanner in = new Scanner(stream);

클래스 로더

클래스 파일에는 가상 머신 명령어가 저장된다. 각 클래스 파일은 단일 클래스나 인터페이스에 해당하는 명령어를 담는다. 클래스 파일을 파일 시스템, JAR 파일, 원격 위치에 둘 수 있고, 심지어 메모리에서 동적으로 생성할 수도 있다. 클래스 로더는 바이트를 로드해서 가상 머신의 클래스나 인터페이스로 변환하는 역할을 한다.

가상 머신은 main메서드가 호출될 메인 메서드 부터 찾아 main을 담는 클래스부터 시작해 필요한 클래스를 각각 로딩하며 이처럼 한 클래스의 로드타임에 필요한 다른 클래스들을 동적 로딩하는 것을 로드타임 동적 로딩이라고 한다.

ClassPath 라는 환경변수에 등록된 디렉토리 또는 클래스들을 JVM에 로드하며, JVM에 로딩된 클래스만 JVM으로 객체를 사용할 수 있다.

자바 프로그램은 세가지 클래스 로더와 연관된다.

  1. 부트스트랩 클래스 로더(bootstrap class loader)는 가장 기본적인 자바 라이브러리 클래스를 로드한다. 이로더는 가상머신의 일부다.
  2. 플랫폼 클래스 로더(platform class loader)는 다른 라이브러리 클래스를 로드한다. 부트스트랩 클래스 로더가 로드하는 클래스와 달리 보안 정책으로 플랫폼 클래스 미션을 구성할 수 있다.
  3. 클래스 로더(System class loader)는 애플리케이션 클래스를 로드한다. 이 로더는 클래스 패스와 모듈 패스에 있는 디렉터리와 JAR 파일에서 클래스를 찾는다.

자세한 클래스로더의 동작 방식은 아래의 링크를 참고 할것

https://homoefficio.github.io/2018/10/13/Java-%ED%81%B4%EB%9E%98%EC%8A%A4%EB%A1%9C%EB%8D%94-%ED%9B%91%EC%96%B4%EB%B3%B4%EA%B8%B0/

https://futurists.tistory.com/43

독자적으로 URLClassLoader 인스턴스를 생성해 클래스 패스에 없는 디렉터리나 JAR 파일에서 클래스를 로드할 수 있다.

           URL[] urls = {
                   new URL("file:///Users/iyeonghan/YH/Often/project/coreJava9/src/main/java/exercise/Ch4/library.jar")};
           File file = new File("/Users/iyeonghan/YH/Often/project/coreJava9/src/main/java/exercise/Ch4/library.jar");

           System.out.println("왜안되"+file.exists());

           try (URLClassLoader loader = new URLClassLoader(urls)) {
               Class<?> cl2 = Class.forName("HelloWorld", true, loader);
           }

컨텍스트 클래스 로더

대부분 클래스 로딩 과정을 신경쓰지 않아도 되지만 클래스는 다른 클래스에서 사용될 때 아무도 모르게 로드된다. 하지만 동적으로 로드하는 메서드가 있고, 또다른 클래스 로더로 로드된 클래스에서 이 메서드를 호출할 경우 문제가 될 수 있다.

  1. 시스템 클래스 로더가 유틸 클래스를 만들고 그 안에서 Class.forName으로 클래스를 로딩하고 있음.
  2. 다른 클래스에서 클래스를 Jar 파일에 있는 클래스를 로딩한다.
  3. jar에 있는 클래스는 Util에 있는 클래스 로딩 메서드로 Jar에 있는 클래스를 로드

클래스 로더는 상위의 클래스로더에 클래스가 존재하는지 확인이 가능하지만, 하위로는 불가능하다는 특징으로 인해 Util의 클래스 로더는 jar의 내용을 알수가 없다. 이 현상을 클래스 로더 역전(classloader inversion)이라고 한다.

한가지 해결 방법은 메서드에 클래스 로더를 전달하는 것이다.

또다른 방법은 컨텍스트 클래스 로더를 사용하는 것이다. 메인 스레드의 컨텍스트 로더는 System ClassLoader를 참조한다. 만약 스레드를 생성하게 되면 해당 스레드의 컨텍스트 로더는 생성하는 쪽의 컨텍스트 클래스 로더로 된다. 결국 모든 스레드의 컨텍스트 로더는 시스템 클래스 로더를 가리키게 된다.

그래서 util의 클래스 로더를 사용하기 이전에, 컨텍스트 클래스 로더를 jar를 로딩하는 로더로 변경하고, util에선 이전에 넣어둔 컨텍스트 로더에서 가져와 사용한다.

서비스 로더

특정 서비스는 프로그램을 조립하거나 배포하는 시점에 구성이 가능해야 한다. 이렇게 만드는 한가지 방법은 서로 다른 서비스 구현체를 사용할 수 있게 하고, 프로그램에서 그중 가장 적합한 구현체를 선택하는 것이다. serviceLoader 클래스를 이용하면 공통 인터페이스를 준수하는 서비스 구현체를 손쉽게 로드할 수 있다.

A service is a well-known set of interfaces and (usually abstract) classes. A service provider is a specific implementation of a service.

리플렉션

프로그램에서 리플렉션을 이용하면 실행 시간에 객체 내용을 조사하고, 해당 객체에 있는 임의의 메서드를 호출할 수 있다. 리플렉션은 객체-관계 매퍼(Object-relational mapper)나 GUI 빌더 같은 도구를 구현할 때 유용하다.

클래스 멤버 나열

java.lang.reflect 패키지에 속한 Field, Method, Constructor 클래스는 각각 클래스의 필드, 메서드, 생성자를 나타낸다.

Field 클래스에는 getType 메서드가 있다. getType 메서드는 필드 타입을 기술하는 Class 타입 객체를 반환한다.

위의 세가지 클래스에는 모두 getModifiers 메서드가 있으며 메서드가 사용된 제어자(public private)를 기술하는 정수를 반환한다.

getFields, getMethods, getConstructors 메서드는 각각 해당 클래스가 지원하는 public 필드, 메서드, 생성자의 배열을 반환한다. 이 배열에는 상속받은 공개 멤버도 포함된다.

getDeclaredFields, getDeclaredMethods, getDeclaredConstructors 메서드는 각각 해당 클래스에 선언된 모든 필드, 메서드, 생성자로 구성된 배열을 반환한다. 이 배열에는 비공개와 패키지, 보호 멤버까지 포함되지만 슈퍼 클래스의 멤버는 포함되지 않는다.

밑의 코드에서 주목할 점은 프로그램을 컴파일한 시점에 이용할 수 있는 클래스뿐만 아니라 자바 가상 머신이 로드할 수 있는 모든 클래스를 이 코드로 분석할 수 있다는 점이다.

Class<?> classs = Class.forName("exercise.Ch4.Employee");

    for (Method m : classs.getDeclaredMethods()) {
        System.out.println(
                Modifier.toString(m.getModifiers()) + " " +
                        m.getReturnType().getCanonicalName() + " " +
                        m.getName() +
                        Arrays.toString(m.getParameters())
        );
    classs = classs.getSuperclass();

하지만 자바 플랫폼 모듈 시스템은 리플렉션을 이용한 접근을 상당히 제한하며 기본적으로 같은 모듈에 속한 클래스만 리플렉션으로 분석할 수 있다.

객체 조사

Field 객체를 이용하면 지정한 필드를 가진 객체의 필드값에 접근할 수 있다. get 메서드는 필드 값이 기본 타입이면 래퍼 객체를 반환하며, 이 경우 get 대신 getInt, getDouble메서드를 이용해 실제 값을 얻어 올 수도 있다.

비공개 Field와 Method 객체를 사용하려면 먼저 객체를 접근 가능하게 만들어야 하는데, SetAccessible(true)를 호출하면 해당 필드 또는 메서드를 리플렉션용으로 ‘잠금 해제’한다.

Object object = employee;

    for (Field f : object.getClass().getDeclaredFields()) {
        f.setAccessible(true);
        try {
            Object value = f.get(obj);
            System.out.println(f.getName() + " : " + value);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

메서드 호출

Field 객체로 객체의 필드를 읽고 쓸 수 있는 것처럼 Method 객체로 객체의 메서드를 호출할 수 있다.

Method m = ...;
Object result = m.invoke(obj, arg1, arg2, ...);

정적 메서드를 호출할 때는 첫 번째 인수로 null을 전달해야한다. 또 원하는 메서드를 얻어 내려면 getMethods나 getDeclaredMethods를 호출한 후 반환받는 배열을 검색하면 된다.

매개변수 타입을 제공하여 getMethod를 호출하는 방법도 있다. Person 객체에서 setName(String) 메서드를 얻어 오는 코드이다.

Person p = ...;
Method m = p.getClass().getMethod("setName", String.class);
m.invoke(obj, "*******");

객체 생성

객체를 생성하려면 먼저 Constructor 객체를 찾은 후 해당 객체의 newInstance 메서드를 호출해야한다.

Constructor constr = cl.getConstructor(int.class);
Object obj = constr.newInstance(42);

자바빈

많은 객체지향언어에서 property(getter, setter를 가진 필드)를 지원하며, 해당 property를 읽는지 쓰는지에 따라 표현식 object.propertyName을 getter 또는 setter 메서드 호출로 매핑한다. 자바에는 이런 문법이 없지만 property가 게터 세터 쌍에 대응하는 규약이 있다. javaBean은 인수 없는 생성자, 게터/세터 쌍, 기타 메서드로 구성된 클래스를 의미한다.

게터세터의 패턴

public Type getProperty()
public void setProperty(Type newValue)

배열 다루기

isArray 메서드는 매개변수로 받은 Class 객체가 배열을 나타내는지 검사한다. Class 객체가 배열을 나타낸다면 getComponentType 메서드는 배열 요소의 타입에 기술하는 Class를 돌려준다.

가득찬 배열의 길이를 늘리는 코드이다. 하지만 이 메서드가 반환하는 배열의 타입은 Object[]다. Object[] 배열은 Person[]배열로 캐스트할 수 없다.

    public static Object badCopyOf(Object[] array, int newLength) {
        Object[] newArray = new Object[newLength];
        for (int i  = 0; i < Math.min(array.length, newLength);i ++){
            newArray[i] = array[i];
        }
        return newArray;
    }

원본 배열과 같은 타입으로 배열을 새로 만들기 위해선 Array 클래스의 newInstance 메서드를 사용해야한다. goodCopyOf의 매개변수 타입은 Object[]가 아니라 Object이다.

 public static Object goodCopyOf(Object array, int newLength) {
        Class<?> cl = array.getClass();
        if (!cl.isArray()) return null;
        Class<?> componentType = cl.getComponentType();
        int length = Array.getLength(array);    //array.length 랑 Array.getLength()랑 차이가 뭘까 ?
        Object newArray = Array.newInstance(componentType, newLength);
        for (int i = 0; i < Math.min(length, newLength); i++)
            Array.set(newArray, i, Array.get(array, i));
        return newArray;
    }
}

프록시

proxy 클래스는 실행 시간에 지정받은 인터페이스 한 개나 일련의 인터페이스를 구현하는 새로운 클래스를 생성할 수 있다. 이런 프록시는 컴파일 시간에 어느 인터페이스를 구현해야 하는지 아직 모를 때만 필요하다.

프록시 클래스는 지정받은 인터페이스에서 요구하는 모든 메서드와 Object 클래스에 정의된 모든 메서드(toString, equals 등)를 가진다. 하지만 실행 시간에 이런 메서드에 대응하는 새로운 코드를 정의하지는 못하므로 호출 핸들러를 전달해야한다.

호출 핸들러(invocation handler)는 InvocationHandler 인터페이스를 구현한 클래스의 객체를 의미한다. 이 인터페이스에는 invoke 메서드 하나만 선언되어 있다.

Object invoke(Object proxy, Method method, Object[] args)

호출 핸들러는 호출을 원격 서버로 전달하거나 디버깅 목적으로 호출을 추적하는 등 다양하게 작동할 수 있다.

프록시 객체를 생성하려면 Proxy 클래스의 newProxyInstance 메서드를 사용해야 한다.

new ProxyInstance 메서드는 다음 세가지 매개변수를 받는다.

  • 클래스 로더 또는 기본 클래스 로더를 사용하는 null
  • class 객체의 배열(구현할 인터페이스 마다 한 개씩)
  • 호출 핸들러
for (int i = 0; i < values.length; i ++) {
            System.out.println();
            Object value = new Integer(i);
            values[i] = Proxy.newProxyInstance(
                    null,
                    value.getClass().getInterfaces(),
                    (Object proxy, Method m, Object[] margs) -> {
                        System.out.println(value + "."+m.getName() + Arrays.toString(margs));
                        return m.invoke(value, margs);
                    }
            );
        }
        Arrays.binarySearch(values, new Integer(500));

Written by@Zero1
This blog is for that I organize what I study and my thinking, feeling and experience.

GitHub