1. 제네릭 개념 도입(제네릭이 필요한 이유)? (권장: java 비디오 튜토리얼)
먼저 다음 짧은 코드를 살펴보겠습니다.
public class GenericTest { public static void main(String[] args) { List list = new ArrayList(); list.add("qqyumidi"); list.add("corn"); list.add(100); for (int i = 0; i < list.size(); i++) { String name = (String) list.get(i); // 1 System.out.println("name:" + name); } } }
define List 유형에는 String 유형의 두 값이 먼저 추가된 다음 Integer 유형의 값이 추가됩니다. 목록의 기본 유형이 개체이기 때문에 이는 완전히 허용됩니다.
다음 루프에서는 이전에 목록에 정수 유형 값을 추가하는 것을 잊었거나 다른 인코딩 이유로 인해 //1과 유사한 오류가 쉽게 발생할 수 있습니다. 컴파일 단계는 정상인데 런타임 중에 "java.lang.ClassCastException" 예외가 발생하기 때문입니다. 따라서 코딩 중에 이러한 오류를 감지하기가 어렵습니다.
위의 코딩 과정에서 우리는 두 가지 주요 문제가 있음을 발견했습니다.
1 객체를 컬렉션에 넣을 때 컬렉션은 이 객체를 기억하지 못합니다. 이 객체를 컬렉션에서 다시 가져오면 객체의 컴파일된 유형이 Object 유형으로 변경되지만 런타임 유형은 여전히 자체 유형입니다.
2. 따라서 //1에서 컬렉션 요소를 꺼낼 때 해당 유형을 특정 대상 유형으로 강제 변환해야 하며 "java.lang.ClassCastException"이 발생하기 쉽습니다. 예외가 발생합니다.
그래서 컬렉션이 컬렉션의 요소 유형을 기억하고 컴파일 중에 문제가 없는 한 "java.lang.ClassCastException"이라는 목표를 달성할 수 있는 방법이 있습니까? 런타임 중에는 예외가 발생하지 않습니까? 대답은 제네릭을 사용하는 것입니다.
2. 제네릭이란 무엇입니까?
Generics, 즉 "매개변수화된 유형"입니다. 매개변수에 관해 가장 익숙한 것은 메소드를 정의할 때 형식적인 매개변수가 있고, 이 메소드를 호출할 때 실제 매개변수가 전달된다는 것입니다.
그럼 매개변수화된 유형을 어떻게 이해하나요? 이름에서 알 수 있듯이, 메소드의 가변 매개변수와 유사하게 원래의 특정 유형에서 유형을 매개변수화합니다. 이때 유형도 매개변수(유형 매개변수라고 할 수 있음)의 형태로 정의됩니다. 그런 다음 유형(유형 인수)을 사용/호출할 때 특정 유형이 전달됩니다.
조금 복잡해 보이는데 먼저 위의 예시를 작성하는 일반적인 방법을 살펴보겠습니다.
public class GenericTest { public static void main(String[] args) { /* List list = new ArrayList(); list.add("qqyumidi"); list.add("corn"); list.add(100); */ List<String> list = new ArrayList<String>(); list.add("qqyumidi"); list.add("corn"); //list.add(100); // 1 提示编译错误 for (int i = 0; i < list.size(); i++) { String name = list.get(i); // 2 System.out.println("name:" + name); } } }
일반 쓰기를 사용한 후 //1에 Integer 유형의 객체를 추가하려고 하면 컴파일 오류가 발생합니다. Listf7e83be87db5cd2d9a8a0b8117b38cd4을 통해 다음 항목만 포함할 수 있는 목록 컬렉션으로 직접 제한됩니다. String 유형이므로 //2에서 강제 유형 변환을 수행할 필요가 없습니다. 왜냐하면 이때 컬렉션은 요소의 유형 정보를 기억할 수 있고 컴파일러는 이미 String 유형임을 확인할 수 있기 때문입니다.
위의 일반 정의와 결합하면 Listf7e83be87db5cd2d9a8a0b8117b38cd4에서 String이 실제 매개변수 유형이라는 것을 알 수 있습니다. 즉, 해당 List 인터페이스에는 유형 매개변수가 포함되어야 합니다. 그리고 get() 메소드의 반환 결과도 바로 이 형식 매개변수의 유형입니다(즉, 해당 수신 유형 실제 매개변수). List 인터페이스의 구체적인 정의를 살펴보겠습니다.
public interface List<E> extends Collection<E> { int size(); boolean isEmpty(); boolean contains(Object o); Iterator<E> iterator(); Object[] toArray(); <T> T[] toArray(T[] a); boolean add(E e); boolean remove(Object o); boolean containsAll(Collection<?> c); boolean addAll(Collection<? extends E> c); boolean addAll(int index, Collection<? extends E> c); boolean removeAll(Collection<?> c); boolean retainAll(Collection<?> c); void clear(); boolean equals(Object o); int hashCode(); E get(int index); E set(int index, E element); void add(int index, E element); E remove(int index); int indexOf(Object o); int lastIndexOf(Object o); ListIterator<E> listIterator(); ListIterator<E> listIterator(int index); List<E> subList(int fromIndex, int toIndex); }
List 인터페이스의 일반 정의를 사용한 후 1a4db2c2c2313771e5742b6debf617a1의 E는 유형 매개변수를 나타냅니다. 특정 유형의 실제 매개변수를 수신하며, 이 인터페이스 정의에서 E가 나타날 때마다 외부에서 수신된 동일한 유형의 실제 매개변수를 나타냅니다.
당연히 ArrayList는 List 인터페이스의 구현 클래스이며 정의 형식은 다음과 같습니다.
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } public E get(int index) { rangeCheck(index); checkForComodification(); return ArrayList.this.elementData(offset + index); } //...省略掉其他具体的定义过程 }
이를 통해 Integer가 추가된 이유를 소스 코드 관점에서 이해할 수 있습니다. //1 유형 객체가 잘못 컴파일되었으며 //2에서 get()으로 얻은 유형은 바로 String 유형입니다.
3. 맞춤형 제네릭 인터페이스, 제네릭 클래스 및 제네릭 메서드
위 내용을 통해 모두가 제네릭의 특정 작업 프로세스를 이해했습니다. 유형. 또한 인터페이스, 클래스 및 메서드도 제네릭을 사용하여 정의하고 그에 따라 사용할 수 있다는 것을 알고 있습니다. 예, 특정 용도에서는 일반 인터페이스, 일반 클래스 및 일반 메서드로 나눌 수 있습니다.
사용자 정의 일반 인터페이스, 일반 클래스 및 일반 메소드는 위 Java 소스 코드의 List 및 ArrayList와 유사합니다. 다음과 같이 제네릭 클래스와 메소드의 가장 간단한 정의를 살펴보겠습니다.
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); System.out.println("name:" + name.getData()); } } class Box<T> { private T data; public Box() { } public Box(T data) { this.data = data; } public T getData() { return data; } }
제네릭 인터페이스, 제네릭 클래스 및 제네릭 메소드의 정의 과정에서 우리는 일반적으로 T, E, K 매개변수 형식을 봅니다. of, V 등은 외부 사용에서 전달된 형식 인수를 받기 때문에 일반 매개변수를 나타내는 데 자주 사용됩니다. 그러면 전달된 다양한 유형 인수에 대해 해당 객체 인스턴스의 유형이 동일하게 생성됩니까?
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); Box<Integer> age = new Box<Integer>(712); System.out.println("name class:" + name.getClass()); // com.qqyumidi.Box System.out.println("age class:" + age.getClass()); // com.qqyumidi.Box System.out.println(name.getClass() == age.getClass()); // true } }
이를 통해 제네릭 클래스를 사용할 때 다른 제네릭 인수가 전달되더라도 실제로는 다른 유형이 생성되지 않는다는 사실을 발견했습니다. 메모리에는 제네릭 클래스가 하나만 있습니다. , 이는 원래의 가장 기본적인 유형입니다(이 예에서는 Box). 물론 논리적으로 우리는 이를 여러 다른 일반 유형으로 이해할 수 있습니다.
이유는 Java에서 제네릭 개념의 목적은 코드 컴파일 단계에서만 작동한다는 것입니다. 컴파일 과정에서 제네릭 결과가 올바르게 검증되면 제네릭이 Type이 됩니다. -관련 정보가 지워집니다. 즉, 성공적으로 컴파일된 클래스 파일에는 일반 정보가 포함되어 있지 않습니다. 일반 정보는 런타임 단계에 들어가지 않습니다.
对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。
四.类型通配符
接着上面的结论,我们知道,Boxc8f01a3f8889dcf657849dd45bc0fc4c和Boxc0f559cc8d56b43654fcbe4aa9df7b4a实际上都是Box类型,现在需要继续探讨一个问题,那么在逻辑上,类似于Boxc8f01a3f8889dcf657849dd45bc0fc4c和Boxc0f559cc8d56b43654fcbe4aa9df7b4a是否可以看成具有父子关系的泛型类型呢?
为了弄清这个问题,我们继续看下下面这个例子:
public class GenericTest { public static void main(String[] args) { Box<Number> name = new Box<Number>(99); Box<Integer> age = new Box<Integer>(712); getData(name); //The method getData(Box<Number>) in the type GenericTest is //not applicable for the arguments (Box<Integer>) getData(age); // 1 } public static void getData(Box<Number> data){ System.out.println("data :" + data.getData()); } }
我们发现,在代码//1处出现了错误提示信息:The method getData(Boxc8f01a3f8889dcf657849dd45bc0fc4c) in the t ype GenericTest is not applicable for the arguments (Boxc0f559cc8d56b43654fcbe4aa9df7b4a)。显然,通过提示信息,我们知道Boxc8f01a3f8889dcf657849dd45bc0fc4c在逻辑上不能视为Boxc0f559cc8d56b43654fcbe4aa9df7b4a的父类。那么,原因何在呢?
public class GenericTest { public static void main(String[] args) { Box<Integer> a = new Box<Integer>(712); Box<Number> b = a; // 1 Box<Float> f = new Box<Float>(3.14f); b.setData(f); // 2 } public static void getData(Box<Number> data) { System.out.println("data :" + data.getData()); } } class Box<T> { private T data; public Box() { } public Box(T data) { setData(data); } public T getData() { return data; } public void setData(T data) { this.data = data; } }
这个例子中,显然//1和//2处肯定会出现错误提示的。在此我们可以使用反证法来进行说明。
假设Boxc8f01a3f8889dcf657849dd45bc0fc4c在逻辑上可以视为Boxc0f559cc8d56b43654fcbe4aa9df7b4a的父类,那么//1和//2处将不会有错误提示了,那么问题就出来了,通过getData()方法取出数据时到底是什么类型呢?Integer? Float? 还是Number?且由于在编程过程中的顺序不可控性,导致在必要的时候必须要进行类型判断,且进行强制类型转换。显然,这与泛型的理念矛盾,因此,在逻辑上Boxc8f01a3f8889dcf657849dd45bc0fc4c不能视为Boxc0f559cc8d56b43654fcbe4aa9df7b4a的父类。
好,那我们回过头来继续看“类型通配符”中的第一个例子,我们知道其具体的错误提示的深层次原因了。那么如何解决呢?总部能再定义一个新的函数吧。
这和Java中的多态理念显然是违背的,因此,我们需要一个在逻辑上可以用来表示同时是Boxc0f559cc8d56b43654fcbe4aa9df7b4a和Boxc8f01a3f8889dcf657849dd45bc0fc4c的父类的一个引用类型,由此,类型通配符应运而生。
类型通配符一般是使用 ? 代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且Box6b3d0130bba23ae47fe2b8e8cddf0195在逻辑上是Boxc0f559cc8d56b43654fcbe4aa9df7b4a、Boxc8f01a3f8889dcf657849dd45bc0fc4c...等所有Boxf4669da5dd7ec4cbc793dba728d85fa1的父类。由此,我们依然可以定义泛型方法,来完成此类需求。
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); Box<Integer> age = new Box<Integer>(712); Box<Number> number = new Box<Number>(314); getData(name); getData(age); getData(number); } public static void getData(Box<?> data) { System.out.println("data :" + data.getData()); } }
有时候,我们还可能听到类型通配符上限和类型通配符下限。具体有是怎么样的呢?
在上面的例子中,如果需要定义一个功能类似于getData()的方法,但对类型实参又有进一步的限制:只能是Number类及其子类。此时,需要用到类型通配符上限。
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); Box<Integer> age = new Box<Integer>(712); Box<Number> number = new Box<Number>(314); getData(name); getData(age); getData(number); //getUpperNumberData(name); // 1 getUpperNumberData(age); // 2 getUpperNumberData(number); // 3 } public static void getData(Box<?> data) { System.out.println("data :" + data.getData()); } public static void getUpperNumberData(Box<? extends Number> data){ System.out.println("data :" + data.getData()); } }
此时,显然,在代码//1处调用将出现错误提示,而//2 //3处调用正常。
类型通配符上限通过形如Boxa2b037db85f4e1df0e812b9647ac55a8形式定义,相对应的,类型通配符下限为Boxda50108ad159903fabe211f1543600e8形式,其含义与类型通配符上限正好相反,在此不作过多阐述了。
更多java知识请关注java基础教程栏目。
위 내용은 Java 제네릭에 대한 자세한 소개의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!