제네릭은 Java에서 매우 중요한 지식 포인트입니다. 제네릭은 Java 컬렉션 클래스 프레임워크에서 널리 사용됩니다. 이 기사에서는 와일드카드 처리 및 성가신 유형 삭제가 포함된 Java 제네릭의 디자인을 처음부터 살펴보겠습니다.
먼저 간단한 Box 클래스를 정의합니다:
public class Box { private String object; public void set(String object) { this.object = object; } public String get() { return object; } }
이것이 가장 일반적인 접근 방식입니다. 이 방법의 단점은 현재는 String 유형 요소만 Box에 로드할 수 있다는 것입니다. 나중에 Integer와 같은 다른 유형의 요소를 로드해야 하는 경우 다른 Box를 다시 작성해야 합니다. 재사용의 경우 제네릭을 사용하면 이 문제를 매우 잘 해결할 수 있습니다.
아아아아이런 방식으로 Box 클래스를 재사용할 수 있으며 T를 원하는 유형으로 바꿀 수 있습니다.
public class Box<T> { // T stands for "Type" private T t; public void set(T t) { this.t = t; } public T get() { return t; } }
제네릭 클래스에 대해 읽은 후 제네릭 메서드를 살펴보겠습니다. 일반 메서드를 선언하는 것은 매우 간단합니다. 반환 유형 앞에 b56561a2c0bc639cf0044c0859afb88f
Box<Integer> integerBox = new Box<Integer>(); Box<Double> doubleBox = new Box<Double>(); Box<String> stringBox = new Box<String>();
와 같은 형식을 추가하면 됩니다. 다음과 같이 일반 메소드를 호출할 수 있습니다:
public class Util { public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) { return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue()); } } public class Pair<K, V> { private K key; private V value; public Pair(K key, V value) { this.key = key; this.value = value; } public void setKey(K key) { this.key = key; } public void setValue(V value) { this.value = value; } public K getKey() { return key; } public V getValue() { return value; } }
또는 Java1.7/1.8의 유형 추론을 사용하여 Java가 해당 유형 매개변수를 자동으로 파생하도록 합니다.
Pair<Integer, String> p1 = new Pair<>(1, "apple"); Pair<Integer, String> p2 = new Pair<>(2, "pear"); boolean same = Util.<Integer, String>compare(p1, p2);
이제 우리는 특정 요소보다 큰 일반 배열의 요소 수를 찾는 함수를 구현하려고 합니다.
Pair<Integer, String> p1 = new Pair<>(1, "apple"); Pair<Integer, String> p2 = new Pair<>(2, "pear"); boolean same = Util.compare(p1, p2);
하지만 이는 분명히 잘못된 것입니다. 왜냐하면 short, int, double, long, float, byte, char 등과 같은 기본 유형을 제외하고 다른 클래스는 > 연산자를 사용하지 못할 수 있으므로 컴파일러가 오류를 보고하므로 이 문제를 해결하는 방법 모직 옷감? 대답은 경계 문자를 사용하는 것입니다.
아아아아다음과 유사한 명령문을 작성하는 것은 유형 매개변수 T가 Comparable 인터페이스를 구현하는 클래스를 나타낸다고 컴파일러에 알리는 것과 같습니다. 이는 컴파일러에게 모든 클래스가 최소한 CompareTo 메소드를 구현한다고 알리는 것과 같습니다.
아아아아와일드카드를 이해하기 전에 먼저 위에서 정의한 Box 클래스를 빌려
public static <T> int countGreaterThan(T[] anArray, T elem) { int count = 0; for (T e : anArray) if (e > elem) // compiler error ++count; return count; }
와 같은 메소드를 추가한다고 가정해 보겠습니다.
그렇다면 이제 Boxc8f01a3f8889dcf657849dd45bc0fc4c는 어떤 유형의 매개변수를 허용합니까? Boxc0f559cc8d56b43654fcbe4aa9df7b4a 또는 Boxeafb63d086dd6c9bd19609d76bcc2869를 전달할 수 있나요? 대답은 '아니오'입니다. Integer와 Double은 Number의 하위 클래스이지만 Box
먼저 아래에서 사용할 몇 가지 간단한 클래스를 정의합니다.
public interface Comparable<T> { public int compareTo(T o); }
다음 예에서는 일반 클래스 Reader를 만든 다음 f1()에서 Fruit f = FruitReader.readExact(apples);를 시도하면 Liste4dae6b035208b28264d9169d0b1fee3 사이에 간격이 있기 때문에 컴파일러가 오류를 보고합니다. Apple> 아무런 관계가 없습니다.
아아아아하지만 우리의 일반적인 사고 습관에 따르면 Apple과 Fruit 사이에는 연결이 있어야 하지만 컴파일러는 이를 인식할 수 없습니다. 그러면 일반 코드에서 이 문제를 어떻게 해결할 수 있을까요? 와일드카드를 사용하여 이 문제를 해결할 수 있습니다.
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) { int count = 0; for (T e : anArray) if (e.compareTo(elem) > 0) ++count; return count; }
이는 FruitReader의 readCovariant 메소드에서 허용하는 매개변수가 Fruit의 하위 클래스(과일 자체 포함)여야만 하므로 하위 클래스와 상위 클래스 간의 관계도 관련된다는 것을 컴파일러에 알리는 것과 같습니다.
위에서 우리는 목록에서 요소를 가져올 수 있는 84ae8caa7f538c9ddbae94fbfaae402c와 유사한 사용법을 보았습니다. 그러면 목록에 요소를 추가할 수 있습니까? 한번 시도해 보세요:
아아아아대답은 '아니요'입니다. Java 컴파일러에서는 이를 허용하지 않습니다. 이유는 무엇입니까? 우리는 컴파일러의 관점에서 이 문제를 고려할 수도 있습니다. Listdf7e3edeea23a9191420c75abedd9ff2 자체에 여러 가지 의미를 가질 수 있기 때문에
public void boxTest(Box<Number> n) { /* ... */ }
Apple을 추가하려고 하면 flist가 새로운 ArrayListb6d994c109c3fd26f54aa9d5ff75e102();
를 가리킬 수 있습니다. Orange를 추가하려고 하면 flist가 새 ArrayList463277d9ebc274bcf30ecc27cb72790a();
을 가리킬 수 있습니다. 과일을 추가하려고 하면 과일은 모든 유형의 과일일 수 있으며, flist는 특정 유형의 과일만 원할 수 있습니다. 컴파일러는 이를 인식할 수 없으며 오류를 보고합니다.
따라서 968a46d75ddf2b01b7014834f43b23fd를 사용할 수 있습니다.
public class GenericWriting { static List<Apple> apples = new ArrayList<Apple>(); static List<Fruit> fruit = new ArrayList<Fruit>(); static <T> void writeExact(List<T> list, T item) { list.add(item); } static void f1() { writeExact(apples, new Apple()); writeExact(fruit, new Apple()); } static <T> void writeWithWildcard(List<? super T> list, T item) { list.add(item) } static void f2() { writeWithWildcard(apples, new Apple()); writeWithWildcard(fruit, new Apple()); } public static void main(String[] args) { f1(); f2(); } }
这样我们可以往容器里面添加元素了,但是使用super的坏处是以后不能get容器里面的元素了,原因很简单,我们继续从编译器的角度考虑这个问题,对于List72b4226105aa1d07ec3b6e98f565c59e list,它可以有下面几种含义:
List<? super Apple> list = new ArrayList<Apple>(); List<? super Apple> list = new ArrayList<Fruit>(); List<? super Apple> list = new ArrayList<Object>();
当我们尝试通过list来get一个Apple的时候,可能会get得到一个Fruit,这个Fruit可以是Orange等其他类型的Fruit。
根据上面的例子,我们可以总结出一条规律,”Producer Extends, Consumer Super”:
“Producer Extends” – 如果你需要一个只读List,用它来produce T,那么使用? extends T。
“Consumer Super” – 如果你需要一个只写List,用它来consume T,那么使用? super T。
如果需要同时读取以及写入,那么我们就不能使用通配符了。
如何阅读过一些Java集合类的源码,可以发现通常我们会将两者结合起来一起用,比如像下面这样:
public class Collections { public static <T> void copy(List<? super T> dest, List<? extends T> src) { for (int i=0; i<src.size(); i++) dest.set(i, src.get(i)); } }
Java泛型中最令人苦恼的地方或许就是类型擦除了,特别是对于有C++经验的程序员。类型擦除就是说Java泛型只能用于在编译期间的静态类型检查,然后编译器生成的代码会擦除相应的类型信息,这样到了运行期间实际上JVM根本就知道泛型所代表的具体类型。这样做的目的是因为Java泛型是1.5之后才被引入的,为了保持向下的兼容性,所以只能做类型擦除来兼容以前的非泛型代码。对于这一点,如果阅读Java集合框架的源码,可以发现有些类其实并不支持泛型。
说了这么多,那么泛型擦除到底是什么意思呢?我们先来看一下下面这个简单的例子:
public class Node<T> { private T data; private Node<T> next; public Node(T data, Node<T> next) } this.data = data; this.next = next; } public T getData() { return data; } // ... }
编译器做完相应的类型检查之后,实际上到了运行期间上面这段代码实际上将转换成:
public class Node { private Object data; private Node next; public Node(Object data, Node next) { this.data = data; this.next = next; } public Object getData() { return data; } // ... }
这意味着不管我们声明Nodef7e83be87db5cd2d9a8a0b8117b38cd4还是Nodec0f559cc8d56b43654fcbe4aa9df7b4a,到了运行期间,JVM统统视为Nodea87fdacec66f0909fc0757c19f2d2b1d。有没有什么办法可以解决这个问题呢?这就需要我们自己重新设置bounds了,将上面的代码修改成下面这样:
public class Node<T extends Comparable<T>> { private T data; private Node<T> next; public Node(T data, Node<T> next) { this.data = data; this.next = next; } public T getData() { return data; } // ... }
这样编译器就会将T出现的地方替换成Comparable而不再是默认的Object了:
public class Node { private Comparable data; private Node next; public Node(Comparable data, Node next) { this.data = data; this.next = next; } public Comparable getData() { return data; } // ... }
上面的概念或许还是比较好理解,但其实泛型擦除带来的问题远远不止这些,接下来我们系统地来看一下类型擦除所带来的一些问题,有些问题在C++的泛型中可能不会遇见,但是在Java中却需要格外小心。
在Java中不允许创建泛型数组,类似下面这样的做法编译器会报错:
List<Integer>[] arrayOfLists = new List<Integer>[2]; // compile-time error
为什么编译器不支持上面这样的做法呢?继续使用逆向思维,我们站在编译器的角度来考虑这个问题。
我们先来看一下下面这个例子:
Object[] strings = new String[2]; strings[0] = "hi"; // OK strings[1] = 100; // An ArrayStoreException is thrown.
对于上面这段代码还是很好理解,字符串数组不能存放整型元素,而且这样的错误往往要等到代码运行的时候才能发现,编译器是无法识别的。接下来我们再来看一下假设Java支持泛型数组的创建会出现什么后果:
Object[] stringLists = new List<String>[]; // compiler error, but pretend it's allowed stringLists[0] = new ArrayList<String>(); // OK // An ArrayStoreException should be thrown, but the runtime can't detect it. stringLists[1] = new ArrayList<Integer>();
假设我们支持泛型数组的创建,由于运行时期类型信息已经被擦除,JVM实际上根本就不知道new ArrayListf7e83be87db5cd2d9a8a0b8117b38cd4()和new ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a()的区别。类似这样的错误假如出现才实际的应用场景中,将非常难以察觉。
如果你对上面这一点还抱有怀疑的话,可以尝试运行下面这段代码:
public class ErasedTypeEquivalence { public static void main(String[] args) { Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); System.out.println(c1 == c2); // true } }
继续复用我们上面的Node的类,对于泛型代码,Java编译器实际上还会偷偷帮我们实现一个Bridge method。
public class Node<T> { public T data; public Node(T data) { this.data = data; } public void setData(T data) { System.out.println("Node.setData"); this.data = data; } } public class MyNode extends Node<Integer> { public MyNode(Integer data) { super(data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } }
看完上面的分析之后,你可能会认为在类型擦除后,编译器会将Node和MyNode变成下面这样:
public class Node { public Object data; public Node(Object data) { this.data = data; } public void setData(Object data) { System.out.println("Node.setData"); this.data = data; } } public class MyNode extends Node { public MyNode(Integer data) { super(data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } }
实际上不是这样的,我们先来看一下下面这段代码,这段代码运行的时候会抛出ClassCastException异常,提示String无法转换成Integer:
MyNode mn = new MyNode(5); Node n = mn; // A raw type - compiler throws an unchecked warning n.setData("Hello"); // Causes a ClassCastException to be thrown. // Integer x = mn.data;
如果按照我们上面生成的代码,运行到第3行的时候不应该报错(注意我注释掉了第4行),因为MyNode中不存在setData(String data)方法,所以只能调用父类Node的setData(Object data)方法,既然这样上面的第3行代码不应该报错,因为String当然可以转换成Object了,那ClassCastException到底是怎么抛出的?
实际上Java编译器对上面代码自动还做了一个处理:
class MyNode extends Node { // Bridge method generated by the compiler public void setData(Object data) { setData((Integer) data); } public void setData(Integer data) { System.out.println("MyNode.setData"); super.setData(data); } // ... }
这也就是为什么上面会报错的原因了,setData((Integer) data);的时候String无法转换成Integer。所以上面第2行编译器提示unchecked warning的时候,我们不能选择忽略,不然要等到运行期间才能发现异常。如果我们一开始加上Nodec0f559cc8d56b43654fcbe4aa9df7b4a n = mn就好了,这样编译器就可以提前帮我们发现错误。
正如我们上面提到的,Java泛型很大程度上只能提供静态类型检查,然后类型的信息就会被擦除,所以像下面这样利用类型参数创建实例的做法编译器不会通过:
public static <E> void append(List<E> list) { E elem = new E(); // compile-time error list.add(elem); }
但是如果某些场景我们想要需要利用类型参数创建实例,我们应该怎么做呢?可以利用反射解决这个问题:
public static <E> void append(List<E> list, Class<E> cls) throws Exception { E elem = cls.newInstance(); // OK list.add(elem); }
我们可以像下面这样调用:
List<String> ls = new ArrayList<>(); append(ls, String.class);
实际上对于上面这个问题,还可以采用Factory和Template两种设计模式解决,感兴趣的朋友不妨去看一下Thinking in Java中第15章中关于Creating instance of types(英文版第664页)的讲解,这里我们就不深入了。
我们无法对泛型代码直接使用instanceof关键字,因为Java编译器在生成代码的时候会擦除所有相关泛型的类型信息,正如我们上面验证过的JVM在运行时期无法识别出ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a和ArrayListf7e83be87db5cd2d9a8a0b8117b38cd4的之间的区别:
public static <E> void rtti(List<E> list) { if (list instanceof ArrayList<Integer>) { // compile-time error // ... } } => { ArrayList<Integer>, ArrayList<String>, LinkedList<Character>, ... }
和上面一样,我们可以使用通配符重新设置bounds来解决这个问题:
public static void rtti(List<?> list) { if (list instanceof ArrayList<?>) { // OK; instanceof requires a reifiable type // ... } }
위 내용은 Java 제네릭에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!