범형의 협동과 역변

11258 단어 Java 기본 사항
다음 코드를 보십시오.
Number num = new Integer(1);  
ArrayList list = new ArrayList(); //type mismatch

List extends Number> list = new ArrayList();
list.add(new Integer(1)); //error
list.add(new Float(1.2f));  //error

Integer는 Number의 하위 클래스입니다. Integer 유형의 실례는 Number 유형의 변수에 값을 부여할 수 있는데 왜 ArrayList는 ArrayList에 값을 부여할 수 없습니까?이것은 자바의 일반 어댑터와 협동과 역변을 이해해야 한다.

협동과 역변


Liskov 교체 지침


모든 인용 기류 (부류) 는 하위 클래스의 대상을 투명하게 사용할 수 있어야 한다.
LSP는 다음과 같은 네 가지 의미로 구성됩니다.
  • 자류는 부류의 방법을 완전히 가지고 구체적인 자류는 부류의 추상적인 방법을 실현해야 한다.
  • 자류에서 자신을 늘릴 수 있는 방법.
  • 자류가 부류를 덮어쓰거나 실현할 때 방법의 형삼은 부류 방법보다 더욱 느슨하다.
  • 하위 클래스가 부모 클래스를 덮어쓰거나 실현할 때 방법의 반환 값은 부모 클래스보다 더욱 엄격하다.

  • 정의


    역변과 협동은 유형 전환(type transformation) 후의 계승 관계를 설명하는데 그 정의는 A, B가 유형을 나타내면 f(⋅)는 유형 전환을 나타내고 ≤는 계승 관계를 나타낸다(예를 들어 A≤B는 A가 B에서 파생된 하위 클래스임을 나타낸다).
    f(\8901)는 역변(contravariant)이고 A≤B일 때 f(B)≤f(A)가 성립된다.
    f(⋅)는 협동(covariant)으로 A≤B일 때 f(A)≤f(B)가 성립된다.
    f(\8901)는 변하지 않는다(invariant). A≤B일 때 상기 두 식은 모두 성립되지 않는다. 즉, f(A)와 f(B)는 상호 간에 상속 관계가 없다.

    유형 협동성


    배열은 협동적이다

    // CovariantArrays.java
    class Fruit {}
    class Apple extends Fruit {}
    class Jonathan extends Apple {}
    class Orange extends Fruit {}
    
    public class CovariantArrays {
        public static void main(String[] args) {
            Fruit[] fruit = new Apple[10];
            fruit[0] = new Apple();
            fruit[1] = new Jonathan();
            try {
                fruit[0] = new Fruit();
            } catch (Exception e) {
                System.out.println(e);
            }
            try {
                fruit[0] = new Orange();
            } catch (Exception e) {
                System.out.println(e);
            }
        }
    }

    fruit 수조는 컴파일하는 동안 컴파일할 수 있습니다.하지만 운행 중 이상이 발생할 수 있습니다.fruit[0]는 Apple 유형이기 때문에 값이 Orange 유형일 때 이상이 발생합니다.

    범형은 변하지 않는다


    메서드


    호출 방법result = method(n);Liskov 교체 원칙에 따라 전입형삼 n의 유형은method형삼의 하위 유형, 즉typeof(n)≤typeof(method's parameter)이어야 한다.result는 method 반환값의 기본 유형이어야 합니다. typeof(methods's return)≤typeof(result)static Number method(Number num) {     return 1; }
    Object result = method(new Integer(2));//correct Number result = method(new Object());//error Integer result = method(new Integer(2));//error
    Java 1.4에서 하위 클래스 덮어쓰기(override) 상위 클래스 메서드에서 참조와 반환 값의 유형은 상위 클래스와 일치해야 합니다.
    class Super {     Number method(Number n) { ... } }
    class Sub extends Super {     @Override     Number method(Number n) { ... } }
    Java 1.5에서 시작하여 하위 클래스가 상위 클래스 메서드를 덮어쓸 때 코디네이션이 보다 구체적인 유형을 반환할 수 있습니다.
    class Super {     Number method(Number n) { ... } }
    class Sub extends Super {     @Override     Integer method(Number n) { ... } }

    어댑터 도입 협동, 역변


    자바의 범용형은 변하지 않지만 때때로 역변과 협동을 실현해야 하는데 어떻게 해야 합니까?이때, 와일드카드?쓸모가 있다.
    은(는) 다음과 같은 일반적인 공동 변화를 구현했습니다.
    List extends Number> list = new ArrayList();
    는 다음과 같은 일반적인 역변을 실현했다.
    List super Number> list = new ArrayList();

    extends와 슈퍼


    왜 (시작 코드에서) List list가dd Integer 및 Float에서 컴파일 오류가 발생합니까?우선dd의 실현을 살펴보자.
    public interface List extends Collection {     boolean add(E e); }
    dd 방법을 호출할 때 일반 E가 자동으로 ,list가 가지고 있는 유형은 Number와 Number파 출산류 중의 어떤 유형이고 그 중에서 Integer 유형을 포함하지만 Integer 유형을 특별히 가리키지 않는다(Integer는 스페어 타이어처럼!!!)그러므로 add Integer 에서 컴파일 오류가 발생했습니다.add 메서드를 호출하기 위해 super 키워드를 사용하여 다음을 수행할 수 있습니다.
    List super Number> list = new ArrayList(); list.add(new Integer(1)); list.add(new Float(1.2f));
    super Number>list가 가지고 있는 유형은 Number와 Number의 기본 클래스 중의 특정한 유형이고 그 중에서 Integer와 Float는 반드시 이 유형의 하위 클래스임을 나타낸다.그래서 dd 방법이 정확하게 호출될 수 있습니다.위의 예에서 보듯이 extends는 범형의 상계를 확정했고 슈퍼는 범형의 하계를 확정했다.

    PECS


    도대체 언제 extends를 사용하고 언제 슈퍼를 사용합니까?Effective Java는 다음과 같은 답을 제공합니다.
    PECS: producer-extends, consumer-super.
    타입이 T생산자를 나타내면 extends T>를, 소비자를 나타내면 슈퍼 T>를 사용한다.
     
    예를 들어, 간단한 Stack API:
    public class  Stack{
        public Stack();
        public void push(E e):
        public E pop();
        public boolean isEmpty();
    }
    pushAll(Iterable src) 방법을 실현하려면 src의 요소를 하나하나 창고에 넣는다.
    public void pushAll(Iterable src){
        for(E e : src)
            push(e)
    }

    실례화Stack의 대상stack이 있다고 가정하면 src는IterableIterable가 있다.pushAll 방법을 호출할 때 type mismatch 오류가 발생합니다. 자바의 일반형은 변할 수 없기 때문에 IterableIterable 모두 Iterable 의 하위 형식이 아닙니다.따라서
    // Wildcard type for parameter that serves as an E producer
    public void pushAll(Iterable extends E> src) {
        for (E e : src)
            push(e);
    }
    popAll(Collection dst) 방법을 실현하려면 Stack의 요소를dd에서dst로 순서대로 꺼내십시오. 만약 어댑터가 필요하지 않으면:
    // popAll method without wildcard type - deficient!
    public void popAll(Collection dst) {
        while (!isEmpty())
            dst.add(pop());   
    }

    마찬가지로 실례화Stack의 대상인stack,dst는Collection; popAll type mismatch , Collection Collection의 하위 유형을 가정한다.따라서 다음과 같이 변경해야 합니다.
    // Wildcard type for parameter that serves as an E consumer
    public void popAll(Collection super E> dst) {
        while (!isEmpty())
            dst.add(pop());
    }

    위 예에서는 pushAll 방법을 호출할 때 E 인스턴스(produces E instances)를 생산했고, popAll 방법을 호출할 때 dst가 E 인스턴스(consumes E instances)를 소비했다.Naftalin과 Wadler는 PECS를 Get and Put Principle이라고 합니다.
    java.util.Collections의 copy 방법(JDK1.7)은 PECS를 완벽하게 표현합니다.
    public static  void copy(List super T> dest, List extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");
    
        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i di=dest.listIterator();
            ListIterator extends T> si=src.listIterator();
            for (int i=0; i

    PECS 요약:

  • 일반 클래스에서 데이터를 가져올 때 extends를 사용한다.
  • 일반 클래스에 데이터를 쓸 때 슈퍼를 사용한다.
  • 가져오고 쓰려면 어댑터를 쓰지 않아도 된다(즉 extends와 슈퍼도 사용하지 않는다).

  •  

    한정된 유형


    이해


    자바 범용형에는 자주 등장하는 관용법이 있는데, 그것은 상당히 이해하기 어렵다.
    class SelfBounded> { // ...
    

    SelfBounded 클래스는 일반 매개 변수 T를 받아들인다. T는 하나의 경계 클래스에 의해 한정된다. 이 경계는 T를 매개 변수로 가진 SelfBounded로 무한 순환으로 보인다.
    먼저 결론을 내렸다. 이런 문법은 하나의 기류를 정의했는데 이 기류는 자류를 매개 변수, 되돌아오는 유형, 작용역으로 사용할 수 있다.이 뜻을 이해하기 위해서 우리는 간단한 판본에서 착수한다.
    // BasicHolder.java
    public class BasicHolder {
        T element;
        void set(T arg) { element = arg; }
        T get() { return element; }
        void f() {
            System.out.println(element.getClass().getSimpleName());
        }
    }
    
    // CRGWithBasicHolder.java
    class Subtype extends BasicHolder {}
    
    public class CRGWithBasicHolder {
        public static void main(String[] args) {
            Subtype st1 = new Subtype(), st2 = new Subtype();
            st1.set(st2);
            Subtype st3 = st1.get();
            st1.f();
        }
    }  
    /*     
    Subtype
    */
    

    새 클래스 Subtype에서 수락된 매개변수와 반환된 값은 기본 클래스 BasicHolder 유형이 아닌 Subtype 유형입니다.그래서 자한정 유형의 본질은 바로 기류가 그 파라미터를 서브류로 대체하는 것이다.이것은 범형 기류가 모든 자류의 공공 기능 모델이 되었지만, 발생하는 클래스에서 기류가 아닌 정확한 유형을 사용한다는 것을 의미한다.따라서 Subtype에서 set()에 전달되는 매개변수와 get()에서 반환되는 유형은 정확히 Subtype입니다.

    자한과 협동


    한정된 유형의 가치는 협변 파라미터 유형을 생성할 수 있다는 데 있다. 방법 파라미터 유형은 하위 클래스에 따라 달라진다.사실 자한정으로 협동 반환 유형이 생길 수 있지만 이것은 중요하지 않다. 왜냐하면 JDK1.5 협동 반환 유형을 도입했다.
    협동 반환 유형
    다음 코드 서브 인터페이스는 기본 인터페이스의 방법을 다시 써서 더 정확한 형식으로 되돌려줍니다.
    // CovariantReturnTypes.java
    class Base {}
    class Derived extends Base {}
    
    interface OrdinaryGetter { 
        Base get();
    }
    
    interface DerivedGetter extends OrdinaryGetter {
        Derived get();
    }
    
    public class CovariantReturnTypes {
        void test(DerivedGetter d) {
            Derived d2 = d.get();
        }
    }
    

    사용자 정의 형식의 기본 클래스를 계승하는 하위 클래스는 위의 get () 처럼 정확한 하위 클래스를 되돌려줍니다.
    // GenericsAndReturnTypes.java
    interface GenericsGetter> {
        T get();
    }
    
    interface Getter extends GenericsGetter {}
    
    public class GenericsAndReturnTypes {
        void test(Getter g) {
            Getter result = g.get();
            GenericsGetter genericsGetter = g.get();
        }
    }
    

    협동 매개변수 유형
    비범용 코드에서 매개 변수 형식은 하위 형식에 따라 변화할 수 없습니다.방법은 다시 불러올 수 있을 뿐 다시 쓸 수 없다.다음 코드 예시를 보십시오.
    // OrdinaryArguments.java
    class OrdinarySetter {
        void set(Base base) {
            System.out.println("OrdinarySetter.set(Base)");
        }
    }
    
    class DerivedSetter extends OrdinarySetter {
        void set(Derived derived) {
            System.out.println("DerivedSetter.set(Derived)");
        }
    }
    
    public class OrdinaryArguments {
        public static void main(String[] args) {
            Base base = new Base();
            Derived derived = new Derived();
            DerivedSetter ds = new DerivedSetter();
            ds.set(derived);
            ds.set(base);
        }
    }
    /*     
    DerivedSetter.set(Derived)
    OrdinarySetter.set(Base)
    */
    

    그러나 한정된 유형을 사용할 때 하위 클래스에는 하나의 방법만 있고 이 방법은 하위 유형이 아닌 하위 유형을 매개 변수로 받아들인다.
    interface SelfBoundSetter> {
        void set(T args);
    }
    
    interface Setter extends SelfBoundSetter {}
    
    public class SelfBoundAndCovariantArguments {
        void testA(Setter s1, Setter s2, SelfBoundSetter sbs) {
            s1.set(s2);
            s1.set(sbs);  //     
        }
    }
    

    변환 캡처


    > 무계 어댑터라고 불리는데 무계 어댑터가 어떤 역할을 하는지 여기서 더 이상 상세하게 설명하지 않겠습니다. 앞에 있는 것을 이해한 학생들은 추측할 수 있을 것입니다.무계 어댑터는 또 하나의 특수한 작용을 한다. 만약에 >을 사용하는 방법으로 원생 유형을 전달한다면 컴파일러에 있어 실제 매개 변수 유형을 추정하여 이 방법을 회전시키고 이 정확한 유형을 사용하는 다른 방법을 호출할 수 있다.이런 기술을 포획전환이라고 부른다.다음 코드는 이런 기술을 보여 주었다.
    public class CaptureConversion {
        static  void f1(Holder holder) {
            T t = holder.get();
            System.out.println(t.getClass().getSimpleName());
        }
        static void f2(Holder> holder) {
            f1(holder);
        }
        @SuppressWarnings("unchecked")
        public static void main(String[] args) {
            Holder raw = new Holder(1);
            f2(raw);
            Holder rawBasic = new Holder();
            rawBasic.set(new Object());
            f2(rawBasic);
            Holder> wildcarded = new Holder(1.0);
            f2(wildcarded);
        }
    }
    /*     
    Integer
    Object
    Double
    */
    

    포획 전환은 이런 상황에서만 가능하다. 즉, 방법 내부에서 정확한 유형을 사용해야 한다.f2 () 에서 T를 되돌릴 수 없습니다. 왜냐하면 T는 f2 () 에 대해 알 수 없기 때문입니다.포획 전환은 흥미롭지만 한계가 있다.

    좋은 웹페이지 즐겨찾기