> Java > java지도 시간 > 본문

Java 배열 공분산 및 일반 불변성에 대한 지식 소개(코드 포함)

不言
풀어 주다: 2019-02-23 16:38:46
앞으로
2471명이 탐색했습니다.

이 기사는 Java 배열 공분산 및 일반 불변성(코드 포함)에 대한 지식을 소개합니다. 필요한 친구가 참고할 수 있기를 바랍니다.

가변성은 OOP 언어 불변성의 큰 함정이며, Java의 배열 공분산은 오래된 함정 중 하나입니다. 최근에 밟았기 때문에 메모를 했습니다. 그런데 패러다임의 퇴보에 대해서도 언급해 봅시다.

배열 공분산을 설명하기 전에 먼저 공분산, 불변성, 반공분산이라는 세 가지 관련 개념을 명확히 하세요.

1. 공분산, 불변성, 반공변성

레스토랑을 위한 코드를 작성했다고 가정해 보겠습니다.

class Soup<T> {
    public void add(T t) {}
}
class Vegetable { }
class Carrot extends Vegetable { }
로그인 후 복사

다음으로 만든 수프에 대한 일반 클래스인 Soup가 있습니다. 성분 T의 메소드 add(T t)는 수프에 성분 T를 추가하는 것을 의미합니다. 야채 클래스는 야채를 나타내고, 당근 클래스는 당근을 나타냅니다. 물론 당근은 야채의 하위 클래스입니다.

그렇다면 수프<야채>와 수프<당근>의 관계는 무엇일까요?

첫 번째 반응은 당근 수프가 분명히 야채 수프이기 때문에 Soup이 Soup의 하위 카테고리여야 한다는 것입니다. 그렇다면 아래 코드를 살펴보세요. 그중 Tomato는 야채

Soup<Vegetable> soup = new Soup<Carrot>();
soup.add(new Tomato());
로그인 후 복사

의 또 다른 하위 클래스인 토마토를 의미합니다. 첫 번째 문장은 괜찮습니다. Soup는 Soup 변수 수프. 두 번째 문장은 문제가 되지 않습니다. 수프는 Soup 유형으로 선언되었으며 해당 add 메소드는 야채 유형의 매개변수를 받고 Tomato는 야채이며 올바른 유형을 갖기 때문입니다.

그런데 두 문장을 합치면 문제가 발생합니다. 실제 수프 유형은 Soup이며, add 메소드에 Tomato 인스턴스를 전달했습니다! 즉, 토마토로 당근 수프를 만들고 있다면 절대 만들 수 없을 것입니다. 따라서 Soup을 Soup의 하위 클래스로 간주하는 것이 논리적으로는 논리적이지만 사용 중에 결함이 있습니다.

그럼 수프<당근>과 수프<야채>는 어떤 관계인가요? 언어마다 이해와 구현이 다릅니다. 정리하자면 세 가지 상황이 있습니다.

(1) Soup이 Soup의 하위 클래스인 경우 일반 Soup는 공변적이라고 합니다.
(2) Soup과 Soup은 서로 관련이 없습니다. 일반 Soup는 불변적이라고 합니다. (3) Soup이 Soup의 상위 클래스인 경우 일반 Soup는 반공변적이라고 합니다. (그러나 반공변성은 일반적이지 않습니다.)

공분산, 불변성, 반공변성의 개념을 이해하고 Java에서의 구현을 살펴보세요. Java의 일반 제네릭은 불변입니다. 즉, Soup과 Soup은 서로 관련이 없는 두 클래스이며 한 클래스의 인스턴스는 다른 클래스의 변수에 할당될 수 없습니다. 따라서 토마토를 사용하여 당근 수프를 만드는 위의 코드는 실제로 전혀 컴파일될 수 없습니다.

2. 배열 공분산

Java에서 배열은 제네릭이 아닌 기본 유형이며 Array와 같은 것은 없습니다. 그러나 다른 유형에서 구축된 유형이라는 점에서 제네릭과 매우 유사합니다. 따라서 배열도 변경 가능한 것으로 간주해야 합니다.

제네릭의 불변성과 달리 Java 배열은

공변성입니다. 즉, Carrot[]는 야채[]의 하위 클래스입니다. 이전 섹션의 예에서는 공분산이 때때로 문제를 일으킬 수 있음을 보여주었습니다. 예를 들어, 다음 코드

Vegetable[] vegetables = new Carrot[10];
vegetables[0] = new Tomato(); // 运行期错误
로그인 후 복사

배열은 공변적이므로 컴파일러에서는 Carrot[10]을 야채[] 유형의 변수에 할당할 수 있으므로 이 코드를 원활하게 컴파일할 수 있습니다. JVM이 실제로 당근 더미에 토마토를 삽입하려고 시도하는 런타임 중에만 큰 문제가 발생합니다. 따라서 위 코드는 런타임 중에 java.lang.ArrayStoreException 유형의 예외를 발생시킵니다.

배열 공분산은 Java의 유명한 역사적 수하물 중 하나입니다. 배열을 사용할 때 주의하세요!

예제의 배열을 List로 바꾸면 상황이 달라집니다. 이렇게

ArrayList<Vegetable> vegetables = new ArrayList<Carrot>(); // 编译期错误
vegetables.add(new Tomato());
로그인 후 복사

ArrayList는 일반 클래스이며 변경할 수 없습니다. 따라서 ArrayList과 ArrayList 사이에는 상속 관계가 없으며 이 코드는 컴파일 중에 오류를 보고합니다.

두 코드 모두 오류를 보고하지만 일반적으로 컴파일 타임 오류가 런타임 오류보다 처리하기가 더 쉽습니다.

3. 제네릭도 공변 및 반공변이 되기를 원하는 경우

제네릭은 변경할 수 없지만 일부 시나리오에서는 여전히 공변이 되기를 원합니다. 예를 들어, 살을 빼기 위해 매일 야채 수프를 마시는 젊은 여성이 있습니다

class Girl {
    public void drink(Soup<Vegetable> soup) {}
}
로그인 후 복사

我们希望drink方法可以接受各种不同的蔬菜汤,包括Soup和Soup。但受到不变性的限制,它们无法作为drink的参数。

要实现这一点,应该采用一种类似于协变性的写法

public void drink(Soup<? extends Vegetable> soup) {}
로그인 후 복사

意思是,参数soup的类型是泛型类Soup,而T是Vegetable的子类(也包括Vegetable自己)。这时,小姐姐终于可以愉快地喝上胡萝卜汤和西红柿汤了。

但是,这种方法有一个限制。编译器只知道泛型参数是Vegetable的子类,却不知道它具体是什么。所以,所有非null的泛型类型参数均被视为不安全的。说起来很拗口,其实很简单。直接上代码

public void drink(Soup<? extends Vegetable> soup) {
    soup.add(new Tomato()); // 错误
    soup.add(null); // 正确}
로그인 후 복사

方法内的第一句会在编译期报错。因为编译器只知道add方法的参数是Vegetable的子类,却不知道它具体是Carrot、Tomato、或者其他的什么类型。这时,传递一个具体类型的实例一律被视为不安全的。即使soup真的是Soup类型也不行,因为soup的具体类型信息是在运行期才能知道的,编译期并不知道。

但是方法内的第二句是正确的。因为参数是null,它可以是任何合法的类型。编译器认为它是安全的。

同样,也有一种类似于逆变的方法

public void drink(Soup<? super Vegetable> soup) {}
로그인 후 복사

这时,Soup中的T必须是Vegetable的父类。

这种情况就不存在上面的限制了,下面的代码毫无问题

public void drink(Soup<? super Vegetable> soup) {
    soup.add(new Tomato());
}
로그인 후 복사

Tomato是Vegetable的子类,自然也是Vegetable父类的子类。所以,编译期就可以确定类型是安全的。

위 내용은 Java 배열 공분산 및 일반 불변성에 대한 지식 소개(코드 포함)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:cnblogs.com
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿