목록 List의 하위 클래스? Java 제네릭이 암시 적으로 다형성이 아닌 이유는 무엇입니까?
Java 제네릭이 상속 / 다형성을 처리하는 방법에 대해 약간 혼란 스럽습니다.
다음 계층을 가정합니다.
동물 (부모)
개 - 고양이 (어린이)
그래서 방법이 있다고 가정 doSomething(List<Animal> animals)
합니다. 상속과 다형성의 규칙 모든함으로써, 나는이 가정 것 List<Dog>
입니다 을 List<Animal>
하고는 List<Cat>
있습니다List<Animal>
그래서 둘 중 하나가이 메서드에 전달 될 수있다 -. 별로. 이 동작을 수행하려면 다음과 같이 말하여 Animal의 하위 클래스 목록을 허용하도록 메서드에 명시 적으로 지시해야합니다 doSomething(List<? extends Animal> animals)
.
이것이 Java의 행동이라는 것을 이해합니다. 내 질문은 왜 ? 왜 다형성은 일반적으로 암시 적이지만 제네릭에 관해서는 지정해야합니까?
아니,이 List<Dog>
입니다 하지List<Animal>
. 당신이 무엇을 할 수 있는지 생각 해보세요. 고양이를 포함해서 어떤 동물 이든List<Animal>
추가 할 수 있습니다 . 이제 논리적으로 강아지 한 떼에 고양이를 추가 할 수 있습니까? 절대적으로하지.
// Illegal code - because otherwise life would be Bad
List<Dog> dogs = new ArrayList<Dog>(); // ArrayList implements List
List<Animal> animals = dogs; // Awooga awooga
animals.add(new Cat());
Dog dog = dogs.get(0); // This should be safe, right?
갑자기 당신은 매우 혼란스러운 고양이 를 갖게 됩니다.
이제는 .a라는 것을 모르기 때문에 a 에 추가 할 수 없습니다 . 값을 검색하고 값이임을 알 수 있지만 임의의 동물을 추가 할 수는 없습니다. 그 반대의 경우도 마찬가지입니다 .이 경우를 안전하게 추가 할 수는 있지만 .Cat
List<? extends Animal>
List<Cat>
Animal
List<? super Animal>
Animal
List<Object>
찾고있는 것을 공변 유형 매개 변수 라고 합니다. 즉, 메서드에서 한 유형의 객체를 다른 유형 Animal
으로 대체 할 수있는 경우 (예 : 으로 대체 될 수 있음 Dog
) 해당 객체를 사용하는 표현식에도 동일하게 적용됩니다 ( List<Animal>
으로 대체 될 수 있음 List<Dog>
). 문제는 일반적으로 가변 목록에 대해 공분산이 안전하지 않다는 것입니다. 당신이 있다고 가정 List<Dog>
하고, 그것은으로 사용되는 List<Animal>
. 고양이를 추가하려고 List<Animal>
하면 List<Dog>
어떻게 되나요? 유형 매개 변수가 공변이되도록 자동으로 허용하면 유형 시스템이 중단됩니다.
형식 매개 변수를 공변으로 지정할 수 있도록 구문을 추가하는 것이 유용합니다. 이렇게하면 ? extends Foo
in 메서드 선언 을 피할 수 있지만 복잡성이 추가됩니다.
a List<Dog>
가 a 가 아닌 이유는 List<Animal>
예를 들어 a Cat
에 a를 삽입 할 수 List<Animal>
있지만 a 에 삽입 할 수 는 없기 때문입니다. List<Dog>
와일드 카드를 사용하여 가능한 경우 제네릭을보다 확장 가능하게 만들 수 있습니다. 예를 들어, a에서 읽는 것은 a List<Dog>
에서 읽는 것과 비슷 List<Animal>
하지만 쓰기는 아닙니다.
Java Tutorials 의 Generics in the Java Language 와 Section on Generics는 왜 어떤 것들이 다형성이거나 제네릭에서 허용되지 않는지에 대해 매우 훌륭하고 심도있는 설명을 제공합니다.
다른 답변이 언급 하는 것에 추가해야한다고 생각하는 요점 은
List<Dog>
자바에서는 아니다List<Animal>
그것은 또한 사실입니다
개 목록은 영어 로 된 동물 목록입니다 (합리적으로 해석)
OP의 직관이 작동하는 방식 (물론 완전히 타당 함)은 후자의 문장입니다. 그러나이 직관을 적용하면 유형 시스템에서 Java-esque가 아닌 언어를 얻게됩니다. 우리 언어가 개 목록에 고양이를 추가하는 것을 허용한다고 가정합니다. 그게 무슨 뜻일까요? 이는 목록이 개 목록이 아니며 단지 동물 목록에 불과하다는 것을 의미합니다. 그리고 포유류의 목록과 네 배의 목록이 있습니다.
다시 말하면 List<Dog>
자바에서 A 는 영어로 "개 목록"을 의미하는 것이 아니라 "개만 가질 수있는 목록"을 의미합니다.
보다 일반적으로 OP의 직관은 객체에 대한 작업이 유형을 변경할 수 있거나 객체의 유형이 해당 값의 (동적) 함수 인 언어에 적합합니다.
Generics의 요점은 그것이 허용하지 않는다는 것입니다. 그러한 유형의 공분산을 허용하는 배열의 상황을 고려하십시오.
Object[] objects = new String[10];
objects[0] = Boolean.FALSE;
이 코드는 잘 컴파일되지만 java.lang.ArrayStoreException: java.lang.Boolean
두 번째 줄에 런타임 오류가 발생 합니다. 형식이 안전하지 않습니다. Generics의 요점은 컴파일 시간 유형 안전성을 추가하는 것입니다. 그렇지 않으면 제네릭없이 일반 클래스를 고수 할 수 있습니다.
이제이 좀 더 유연하게해야 할 시간은 그리고 그 무엇이다 ? super Class
와 ? extends Class
에 대한 것입니다. 전자는 유형 Collection
(예 :)에 삽입해야 할 때이고 후자는 유형 안전 방식으로 읽어야 할 때입니다. 그러나 동시에 두 가지를 모두 수행하는 유일한 방법은 특정 유형을 갖는 것입니다.
문제를 이해하려면 배열과 비교하는 것이 유용합니다.
List<Dog>
의 하위 클래스 가 아닙니다List<Animal>
.
그러나 Dog[]
이다 의 서브 클래스 Animal[]
.
배열은 수정 가능 하고 공변 합니다.
Reifiable 은 유형 정보가 런타임에 완전히 사용 가능함을 의미합니다.
따라서 배열은 런타임 유형 안전성을 제공하지만 컴파일 타임 유형 안전성은 제공하지 않습니다.
// All compiles but throws ArrayStoreException at runtime at last line
Dog[] dogs = new Dog[10];
Animal[] animals = dogs; // compiles
animals[0] = new Cat(); // throws ArrayStoreException at runtime
제네릭의 경우 그 반대입니다.
제네릭은 지워지고 불변 합니다.
따라서 제네릭은 런타임 유형 안전성을 제공 할 수 없지만 컴파일 타임 유형 안전성을 제공합니다.
아래 코드에서 제네릭이 공변 인 경우 라인 3에서 힙 오염 을 만들 수 있습니다 .
List<Dog> dogs = new ArrayList<>();
List<Animal> animals = dogs; // compile-time error, otherwise heap pollution
animals.add(new Cat());
이러한 동작의 기본 논리 Generics
는 유형 삭제 메커니즘 을 따르는 것입니다. 따라서 런타임에 이러한 삭제 프로세스가없는 경우와 collection
달리 유형을 식별 할 수있는 방법 arrays
이 없습니다. 다시 질문으로 돌아 오면 ...
따라서 아래와 같은 방법이 있다고 가정합니다.
add(List<Animal>){
//You can add List<Dog or List<Cat> and this will compile as per rules of polymorphism
}
이제 Java가 호출자가이 메소드에 Animal 유형의 목록을 추가 할 수 있도록 허용하면 잘못된 것을 콜렉션에 추가 할 수 있으며 런타임에도 유형 삭제로 인해 실행됩니다. 배열의 경우 이러한 시나리오에 대한 런타임 예외가 발생합니다.
따라서 본질적으로이 동작은 수집에 잘못된 것을 추가 할 수 없도록 구현됩니다. 이제는 제네릭없이 레거시 자바와의 호환성을 제공하기 위해 유형 삭제가 존재한다고 믿습니다 ....
여기에 제공된 답변은 나를 완전히 설득하지 못했습니다. 그래서 대신 다른 예를 들어 보겠습니다.
public void passOn(Consumer<Animal> consumer, Supplier<Animal> supplier) {
consumer.accept(supplier.get());
}
잘 들리지 않나요? 그러나 s에 대해 s Consumer
와 Supplier
s 만 전달할 수 있습니다 Animal
. 당신이 경우 Mammal
소비자하지만, Duck
공급 업체 모두 동물이 있지만, 그들은 맞지한다. 이를 허용하지 않기 위해 추가 제한 사항이 추가되었습니다.
위의 대신 우리가 사용하는 유형 간의 관계를 정의해야합니다.
예 :
public <A extends Animal> void passOn(Consumer<A> consumer, Supplier<? extends A> supplier) {
consumer.accept(supplier.get());
}
소비자에게 적합한 유형의 물건을 제공하는 공급자 만 사용할 수 있도록합니다.
OTOH, 우리는 할 수 있습니다
public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<A> supplier) {
consumer.accept(supplier.get());
}
다른 방법으로 이동하는 경우 :의 유형을 정의 Supplier
하고 Consumer
.
우리는 할 수 있습니다
public <A extends Animal> void passOn(Consumer<? super A> consumer, Supplier<? extends A> supplier) {
consumer.accept(supplier.get());
}
직관적 인 관계를 가진 경우, Life
-> Animal
-> Mammal
-> Dog
, Cat
등, 우리는 심지어 넣을 수 Mammal
으로 Life
소비자 아니지만 String
에 Life
소비자.
실제로 인터페이스를 사용하여 원하는 것을 얻을 수 있습니다.
public interface Animal {
String getName();
String getVoice();
}
public class Dog implements Animal{
@Override
String getName(){return "Dog";}
@Override
String getVoice(){return "woof!";}
}
그런 다음 다음을 사용하여 컬렉션을 사용할 수 있습니다.
List <Animal> animalGroup = new ArrayList<Animal>();
animalGroup.add(new Dog());
매개 변수화 된 유형에 대한 하위 유형 은 변하지 않습니다 . 클래스 Dog
가의 하위 Animal
유형 List<Dog>
이지만 매개 변수화 된 유형 은의 하위 유형 이 아닙니다 List<Animal>
. 반대로 공변 하위 유형은 배열에서 사용되므로 배열 유형 Dog[]
은의 하위 유형입니다 Animal[]
.
Invariant subtyping은 Java에서 적용하는 유형 제약 조건을 위반하지 않도록합니다. @Jon Skeet이 제공 한 다음 코드를 고려하십시오.
List<Dog> dogs = new ArrayList<Dog>(1);
List<Animal> animals = dogs;
animals.add(new Cat()); // compile-time error
Dog dog = dogs.get(0);
@Jon Skeet이 언급했듯이이 코드는 불법입니다. 그렇지 않으면 개가 예상 할 때 고양이를 반환하여 유형 제약 조건을 위반할 수 있기 때문입니다.
위의 내용을 배열에 대한 유사한 코드와 비교하는 것이 좋습니다.
Dog[] dogs = new Dog[1];
Object[] animals = dogs;
animals[0] = new Cat(); // run-time error
Dog dog = dogs[0];
코드는 합법적입니다. 그러나 배열 저장소 예외가 발생 합니다. 배열은 런타임에 유형을 전달하므로 JVM이 공변 하위 유형의 유형 안전성을 적용 할 수 있습니다.
이를 더 이해하기 위해 javap
아래 클래스에서 생성 된 바이트 코드를 살펴 보겠습니다 .
import java.util.ArrayList;
import java.util.List;
public class Demonstration {
public void normal() {
List normal = new ArrayList(1);
normal.add("lorem ipsum");
}
public void parameterized() {
List<String> parameterized = new ArrayList<>(1);
parameterized.add("lorem ipsum");
}
}
명령을 사용하면 javap -c Demonstration
다음과 같은 Java 바이트 코드가 표시됩니다.
Compiled from "Demonstration.java"
public class Demonstration {
public Demonstration();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void normal();
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: iconst_1
5: invokespecial #3 // Method java/util/ArrayList."<init>":(I)V
8: astore_1
9: aload_1
10: ldc #4 // String lorem ipsum
12: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
17: pop
18: return
public void parameterized();
Code:
0: new #2 // class java/util/ArrayList
3: dup
4: iconst_1
5: invokespecial #3 // Method java/util/ArrayList."<init>":(I)V
8: astore_1
9: aload_1
10: ldc #4 // String lorem ipsum
12: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
17: pop
18: return
}
메서드 본문의 번역 된 코드가 동일한 지 확인하십시오. 컴파일러는 매개 변수화 된 각 유형을 삭제 로 대체했습니다 . 이 속성은 이전 버전과의 호환성을 깨지 않았 음을 의미합니다.
결론적으로 컴파일러는 매개 변수화 된 각 유형을 삭제로 대체하기 때문에 매개 변수화 된 유형에 대해서는 런타임 안전이 불가능합니다. 이로 인해 매개 변수화 된 유형은 구문 설탕에 지나지 않습니다.
목록 항목이 해당 수퍼 유형의 하위 클래스라고 확신하는 경우 다음 접근 방식을 사용하여 목록을 캐스팅 할 수 있습니다.
(List<Animal>) (List<?>) dogs
생성자에 목록을 전달하거나 목록을 반복하려는 경우 유용합니다.
대답 뿐만 아니라 다른 답변이 정확합니다. 나는 도움이 될 것이라고 생각하는 해결책으로 그 대답에 추가 할 것입니다. 나는 이것이 프로그래밍에서 자주 발생한다고 생각합니다. 한 가지 주목할 점은 컬렉션 (목록, 세트 등)의 주요 문제는 컬렉션에 추가하는 것입니다. 그것이 문제가되는 곳입니다. 제거해도 괜찮습니다.
대부분의 경우 Collection<? extends T>
대신 사용할 수 Collection<T>
있으며 이것이 첫 번째 선택이어야합니다. 그러나 그렇게하는 것이 쉽지 않은 경우를 찾고 있습니다. 그것이 항상 최선의 방법인지에 대한 논쟁의 여지가 있습니다. 내가 여기에 변환 a를 취할 수있는 클래스 DownCastCollection 제시하고 Collection<? extends T>
A를 Collection<T>
표준 접근 방식을 사용하는 경우 사용하는 것은 (우리는 목록, 설정, 해 NavigableSet .. 비슷한 클래스를 정의 할 수 있습니다) 매우 불편하다. 다음은 사용 방법의 예입니다 ( Collection<? extends Object>
이 경우 에도 사용할 수 있지만 DownCastCollection 사용을 설명하기 위해 간단하게 유지하겠습니다.
/**Could use Collection<? extends Object> and that is the better choice.
* But I am doing this to illustrate how to use DownCastCollection. **/
public static void print(Collection<Object> col){
for(Object obj : col){
System.out.println(obj);
}
}
public static void main(String[] args){
ArrayList<String> list = new ArrayList<>();
list.addAll(Arrays.asList("a","b","c"));
print(new DownCastCollection<Object>(list));
}
이제 수업 :
import java.util.AbstractCollection;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
public class DownCastCollection<E> extends AbstractCollection<E> implements Collection<E> {
private Collection<? extends E> delegate;
public DownCastCollection(Collection<? extends E> delegate) {
super();
this.delegate = delegate;
}
@Override
public int size() {
return delegate ==null ? 0 : delegate.size();
}
@Override
public boolean isEmpty() {
return delegate==null || delegate.isEmpty();
}
@Override
public boolean contains(Object o) {
if(isEmpty()) return false;
return delegate.contains(o);
}
private class MyIterator implements Iterator<E>{
Iterator<? extends E> delegateIterator;
protected MyIterator() {
super();
this.delegateIterator = delegate == null ? null :delegate.iterator();
}
@Override
public boolean hasNext() {
return delegateIterator != null && delegateIterator.hasNext();
}
@Override
public E next() {
if(!hasNext()) throw new NoSuchElementException("The iterator is empty");
return delegateIterator.next();
}
@Override
public void remove() {
delegateIterator.remove();
}
}
@Override
public Iterator<E> iterator() {
return new MyIterator();
}
@Override
public boolean add(E e) {
throw new UnsupportedOperationException();
}
@Override
public boolean remove(Object o) {
if(delegate == null) return false;
return delegate.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
if(delegate==null) return false;
return delegate.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
throw new UnsupportedOperationException();
}
@Override
public boolean removeAll(Collection<?> c) {
if(delegate == null) return false;
return delegate.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
if(delegate == null) return false;
return delegate.retainAll(c);
}
@Override
public void clear() {
if(delegate == null) return;
delegate.clear();
}
}
JavaSE 자습서 의 예제를 살펴 보겠습니다.
public abstract class Shape {
public abstract void draw(Canvas c);
}
public class Circle extends Shape {
private int x, y, radius;
public void draw(Canvas c) {
...
}
}
public class Rectangle extends Shape {
private int x, y, width, height;
public void draw(Canvas c) {
...
}
}
따라서 개 (원) 목록이 암시 적으로 동물 (모양) 목록으로 간주되어서는 안되는 이유는 다음과 같은 상황 때문입니다.
// drawAll method call
drawAll(circleList);
public void drawAll(List<Shape> shapes) {
shapes.add(new Rectangle());
}
So Java "architects" had 2 options which address this problem:
do not consider that a subtype is implicitly it's supertype, and give a compile error, like it happens now
consider the subtype to be it's supertype and restrict at compile the "add" method (so in the drawAll method, if a list of circles, subtype of shape, would be passed, the compiler should detected that and restrict you with compile error into doing that).
For obvious reasons, that chose the first way.
We should also take in consideration how the compiler threats the generic classes: in "instantiates" a different type whenever we fill the generic arguments.
Thus we have ListOfAnimal
, ListOfDog
, ListOfCat
, etc, which are distinct classes that end up being "created" by the compiler when we specify the generic arguments. And this is a flat hierarchy (actually regarding to List
is not a hierarchy at all).
Another argument why covariance doesn't make sense in case of generic classes is the fact that at base all classes are the same - are List
instances. Specialising a List
by filling the generic argument doesn't extend the class, it just makes it work for that particular generic argument.
The problem has been well-identified. But there's a solution; make doSomething generic:
<T extends Animal> void doSomething<List<T> animals) {
}
now you can call doSomething with either List<Dog> or List<Cat> or List<Animal>.
another solution is to build a new list
List<Dog> dogs = new ArrayList<Dog>();
List<Animal> animals = new ArrayList<Animal>(dogs);
animals.add(new Cat());
'Programing' 카테고리의 다른 글
Bash에서 인덱스를 지정하지 않고 배열에 새 요소 추가 (0) | 2020.09.30 |
---|---|
Solr 대 ElasticSearch (0) | 2020.09.30 |
ViewPager에서 조각이 표시되는시기를 확인하는 방법 (0) | 2020.09.30 |
MySQL 오류 코드 : MySQL Workbench에서 업데이트 중 1175 (0) | 2020.09.30 |
__proto__ VS. (0) | 2020.09.30 |