이 기사는 Java 배열 공분산 및 일반 불변성(코드 포함)에 대한 지식을 소개합니다. 필요한 친구가 참고할 수 있기를 바랍니다.
가변성은 OOP 언어 불변성의 큰 함정이며, Java의 배열 공분산은 오래된 함정 중 하나입니다. 최근에 밟았기 때문에 메모를 했습니다. 그런데 패러다임의 퇴보에 대해서도 언급해 봅시다.
배열 공분산을 설명하기 전에 먼저 공분산, 불변성, 반공분산이라는 세 가지 관련 개념을 명확히 하세요.
1. 공분산, 불변성, 반공변성
레스토랑을 위한 코드를 작성했다고 가정해 보겠습니다.
class Soup<T> { public void add(T t) {} } class Vegetable { } class Carrot extends Vegetable { }
다음으로 만든 수프에 대한 일반 클래스인 Soup8742468051c85b06f0a0af9e3e506b5c가 있습니다. 성분 T의 메소드 add(T t)는 수프에 성분 T를 추가하는 것을 의미합니다. 야채 클래스는 야채를 나타내고, 당근 클래스는 당근을 나타냅니다. 물론 당근은 야채의 하위 클래스입니다.
그렇다면 수프1b0ceddbc40cbc5004928d3bc85c4fc1와 수프6db879f11e07f5f3130821a11cbf6224의 관계는 무엇일까요?
첫 번째 반응은 당근 수프가 분명히 야채 수프이기 때문에 Soup
Soup<Vegetable> soup = new Soup<Carrot>(); soup.add(new Tomato());
의 또 다른 하위 클래스인 토마토를 의미합니다. 첫 번째 문장은 괜찮습니다. Soup48d44884ab571fbfd69a92f91c198045는 Soup48d44884ab571fbfd69a92f91c198045 변수 수프. 두 번째 문장은 문제가 되지 않습니다. 수프는 Soupf154463d24280b51e227a9971c008850 유형으로 선언되었으며 해당 add 메소드는 야채 유형의 매개변수를 받고 Tomato는 야채이며 올바른 유형을 갖기 때문입니다.
그런데 두 문장을 합치면 문제가 발생합니다. 실제 수프 유형은 Soup48d44884ab571fbfd69a92f91c198045이며, add 메소드에 Tomato 인스턴스를 전달했습니다! 즉, 토마토로 당근 수프를 만들고 있다면 절대 만들 수 없을 것입니다. 따라서 Soup
그럼 수프과 수프는 어떤 관계인가요? 언어마다 이해와 구현이 다릅니다. 정리하자면 세 가지 상황이 있습니다.
(1) Soup
(2) Soup
2. 배열 공분산
Java에서 배열은 제네릭이 아닌 기본 유형이며 Array8742468051c85b06f0a0af9e3e506b5c와 같은 것은 없습니다. 그러나 다른 유형에서 구축된 유형이라는 점에서 제네릭과 매우 유사합니다. 따라서 배열도 변경 가능한 것으로 간주해야 합니다. 제네릭의 불변성과 달리 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
3. 제네릭도 공변 및 반공변이 되기를 원하는 경우
제네릭은 변경할 수 없지만 일부 시나리오에서는 여전히 공변이 되기를 원합니다. 예를 들어, 살을 빼기 위해 매일 야채 수프를 마시는 젊은 여성이 있습니다class Girl { public void drink(Soup<Vegetable> soup) {} }
我们希望drink方法可以接受各种不同的蔬菜汤,包括Soup48d44884ab571fbfd69a92f91c198045和Soup8ad9f592c4c8b60fa486d2a1b7041744。但受到不变性的限制,它们无法作为drink的参数。
要实现这一点,应该采用一种类似于协变性的写法
public void drink(Soup<? extends Vegetable> soup) {}
意思是,参数soup的类型是泛型类Soup8742468051c85b06f0a0af9e3e506b5c,而T是Vegetable的子类(也包括Vegetable自己)。这时,小姐姐终于可以愉快地喝上胡萝卜汤和西红柿汤了。
但是,这种方法有一个限制。编译器只知道泛型参数是Vegetable的子类,却不知道它具体是什么。所以,所有非null的泛型类型参数均被视为不安全的。说起来很拗口,其实很简单。直接上代码
public void drink(Soup<? extends Vegetable> soup) { soup.add(new Tomato()); // 错误 soup.add(null); // 正确}
方法内的第一句会在编译期报错。因为编译器只知道add方法的参数是Vegetable的子类,却不知道它具体是Carrot、Tomato、或者其他的什么类型。这时,传递一个具体类型的实例一律被视为不安全的。即使soup真的是Soup8ad9f592c4c8b60fa486d2a1b7041744类型也不行,因为soup的具体类型信息是在运行期才能知道的,编译期并不知道。
但是方法内的第二句是正确的。因为参数是null,它可以是任何合法的类型。编译器认为它是安全的。
同样,也有一种类似于逆变的方法
public void drink(Soup<? super Vegetable> soup) {}
这时,Soup8742468051c85b06f0a0af9e3e506b5c中的T必须是Vegetable的父类。
这种情况就不存在上面的限制了,下面的代码毫无问题
public void drink(Soup<? super Vegetable> soup) { soup.add(new Tomato()); }
Tomato是Vegetable的子类,自然也是Vegetable父类的子类。所以,编译期就可以确定类型是安全的。
위 내용은 Java 배열 공분산 및 일반 불변성에 대한 지식 소개(코드 포함)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!